#include #include #include "util/platform.h" #define TIMING_TIME 0 #define TIMING_FRAME 1 enum matte_layout { MATTE_LAYOUT_HORIZONTAL, MATTE_LAYOUT_VERTICAL, MATTE_LAYOUT_SEPARATE_FILE, MATTE_LAYOUT_MASK, }; enum fade_style { FADE_STYLE_FADE_OUT_FADE_IN, FADE_STYLE_CROSS_FADE }; struct stinger_info { obs_source_t *source; obs_source_t *media_source; obs_source_t *matte_source; uint64_t duration_ns; uint64_t duration_frames; uint64_t transition_point_ns; uint64_t transition_point_frame; float transition_point; float transition_a_mul; float transition_b_mul; bool transitioning; bool transition_point_is_frame; int monitoring_type; enum fade_style fade_style; bool track_matte_enabled; int matte_layout; float matte_width_factor; float matte_height_factor; bool invert_matte; bool do_texrender; bool matte_rendered; gs_effect_t *matte_effect; gs_eparam_t *ep_a_tex; gs_eparam_t *ep_b_tex; gs_eparam_t *ep_matte_tex; gs_eparam_t *ep_invert_matte; gs_texrender_t *matte_tex; gs_texrender_t *stinger_tex; float (*mix_a)(void *data, float t); float (*mix_b)(void *data, float t); }; static const char *stinger_get_name(void *type_data) { UNUSED_PARAMETER(type_data); return obs_module_text("StingerTransition"); } static float mix_a_fade_in_out(void *data, float t); static float mix_b_fade_in_out(void *data, float t); static float mix_a_cross_fade(void *data, float t); static float mix_b_cross_fade(void *data, float t); static void stinger_update(void *data, obs_data_t *settings) { struct stinger_info *s = data; const char *path = obs_data_get_string(settings, "path"); bool hw_decode = obs_data_get_bool(settings, "hw_decode"); bool preload = obs_data_get_bool(settings, "preload"); obs_data_t *media_settings = obs_data_create(); obs_data_set_string(media_settings, "local_file", path); obs_data_set_bool(media_settings, "hw_decode", hw_decode); obs_data_set_bool(media_settings, "looping", false); obs_data_set_bool(media_settings, "full_decode", preload); obs_data_set_bool(media_settings, "is_stinger", true); obs_data_set_bool(media_settings, "is_track_matte", s->track_matte_enabled); obs_source_release(s->media_source); struct dstr name; dstr_init_copy(&name, obs_source_get_name(s->source)); dstr_cat(&name, " (Stinger)"); s->media_source = obs_source_create_private("ffmpeg_source", name.array, media_settings); dstr_free(&name); obs_data_release(media_settings); int64_t point = obs_data_get_int(settings, "transition_point"); s->transition_point_is_frame = obs_data_get_int(settings, "tp_type") == TIMING_FRAME; if (s->transition_point_is_frame) s->transition_point_frame = (uint64_t)point; else s->transition_point_ns = (uint64_t)(point * 1000000LL); bool track_matte_was_enabled = s->track_matte_enabled; s->track_matte_enabled = obs_data_get_bool(settings, "track_matte_enabled"); s->matte_layout = (int)obs_data_get_int(settings, "track_matte_layout"); s->matte_width_factor = (s->matte_layout == MATTE_LAYOUT_HORIZONTAL ? 2.0f : 1.0f); s->matte_height_factor = (s->matte_layout == MATTE_LAYOUT_VERTICAL ? 2.0f : 1.0f); s->invert_matte = obs_data_get_bool(settings, "invert_matte"); s->do_texrender = s->track_matte_enabled && s->matte_layout < MATTE_LAYOUT_SEPARATE_FILE; if (s->matte_source) { obs_source_release(s->matte_source); s->matte_source = NULL; } if (s->track_matte_enabled && s->matte_layout == MATTE_LAYOUT_SEPARATE_FILE) { const char *tm_path = obs_data_get_string(settings, "track_matte_path"); obs_data_t *tm_media_settings = obs_data_create(); obs_data_set_string(tm_media_settings, "local_file", tm_path); obs_data_set_bool(tm_media_settings, "looping", false); s->matte_source = obs_source_create_private( "ffmpeg_source", NULL, tm_media_settings); obs_data_release(tm_media_settings); // no need to output sound from the matte video obs_source_set_muted(s->matte_source, true); } s->monitoring_type = (int)obs_data_get_int(settings, "audio_monitoring"); obs_source_set_monitoring_type(s->media_source, s->monitoring_type); s->fade_style = (enum fade_style)obs_data_get_int(settings, "audio_fade_style"); switch (s->fade_style) { default: case FADE_STYLE_FADE_OUT_FADE_IN: s->mix_a = mix_a_fade_in_out; s->mix_b = mix_b_fade_in_out; break; case FADE_STYLE_CROSS_FADE: s->mix_a = mix_a_cross_fade; s->mix_b = mix_b_cross_fade; break; } if (s->track_matte_enabled != track_matte_was_enabled) { obs_enter_graphics(); gs_texrender_destroy(s->matte_tex); gs_texrender_destroy(s->stinger_tex); s->matte_tex = NULL; s->stinger_tex = NULL; if (s->track_matte_enabled) { s->matte_tex = gs_texrender_create(GS_RGBA, GS_ZS_NONE); s->stinger_tex = gs_texrender_create(GS_RGBA, GS_ZS_NONE); } obs_leave_graphics(); } } static void *stinger_create(obs_data_t *settings, obs_source_t *source) { struct stinger_info *s = bzalloc(sizeof(*s)); s->source = source; s->mix_a = mix_a_fade_in_out; s->mix_b = mix_b_fade_in_out; char *effect_file = obs_module_file("stinger_matte_transition.effect"); char *error_string = NULL; obs_enter_graphics(); s->matte_effect = gs_effect_create_from_file(effect_file, &error_string); obs_leave_graphics(); if (!s->matte_effect) { blog(LOG_ERROR, "Could not open stinger_matte_transition.effect: %s", error_string); bfree(error_string); bfree(s); return NULL; } bfree(effect_file); s->ep_a_tex = gs_effect_get_param_by_name(s->matte_effect, "a_tex"); s->ep_b_tex = gs_effect_get_param_by_name(s->matte_effect, "b_tex"); s->ep_matte_tex = gs_effect_get_param_by_name(s->matte_effect, "matte_tex"); s->ep_invert_matte = gs_effect_get_param_by_name(s->matte_effect, "invert_matte"); obs_transition_enable_fixed(s->source, true, 0); obs_source_update(source, settings); return s; } static void stinger_destroy(void *data) { struct stinger_info *s = data; obs_source_release(s->media_source); obs_source_release(s->matte_source); obs_enter_graphics(); gs_texrender_destroy(s->matte_tex); gs_texrender_destroy(s->stinger_tex); gs_effect_destroy(s->matte_effect); obs_leave_graphics(); bfree(s); } static void stinger_defaults(obs_data_t *settings) { obs_data_set_default_bool(settings, "hw_decode", true); } static void stinger_matte_render(void *data, gs_texture_t *a, gs_texture_t *b, float t, uint32_t cx, uint32_t cy) { struct stinger_info *s = data; struct vec4 background; vec4_zero(&background); obs_source_t *matte_source = (s->matte_layout == MATTE_LAYOUT_SEPARATE_FILE ? s->matte_source : s->media_source); float matte_cx = (float)obs_source_get_width(matte_source) / s->matte_width_factor; float matte_cy = (float)obs_source_get_height(matte_source) / s->matte_height_factor; float width_offset = (s->matte_layout == MATTE_LAYOUT_HORIZONTAL ? (-matte_cx) : 0.0f); float height_offset = (s->matte_layout == MATTE_LAYOUT_VERTICAL ? (-matte_cy) : 0.0f); // Track matte media render if (matte_cx > 0 && matte_cy > 0) { float scale_x = (float)cx / matte_cx; float scale_y = (float)cy / matte_cy; const enum gs_color_space space = obs_source_get_color_space(matte_source, 0, NULL); enum gs_color_format format = gs_get_format_from_space(space); if (gs_texrender_get_format(s->matte_tex) != format) { gs_texrender_destroy(s->matte_tex); s->matte_tex = gs_texrender_create(format, GS_ZS_NONE); } if (gs_texrender_begin_with_color_space(s->matte_tex, cx, cy, space)) { gs_matrix_scale3f(scale_x, scale_y, 1.0f); gs_matrix_translate3f(width_offset, height_offset, 0.0f); gs_clear(GS_CLEAR_COLOR, &background, 0.0f, 0); gs_ortho(0.0f, (float)cx, 0.0f, (float)cy, -100.0f, 100.0f); obs_source_video_render(matte_source); gs_texrender_end(s->matte_tex); } } const bool previous = gs_framebuffer_srgb_enabled(); gs_enable_framebuffer_srgb(true); /* texture setters look reversed, but they aren't */ const char *tech_name = "StingerMatte"; if (gs_get_color_space() == GS_CS_SRGB) { /* users want nonlinear fade */ gs_effect_set_texture(s->ep_a_tex, a); gs_effect_set_texture(s->ep_b_tex, b); } else { /* nonlinear fade is too wrong, so use linear fade */ gs_effect_set_texture_srgb(s->ep_a_tex, a); gs_effect_set_texture_srgb(s->ep_b_tex, b); tech_name = "StingerMatteLinear"; } gs_effect_set_texture(s->ep_matte_tex, gs_texrender_get_texture(s->matte_tex)); gs_effect_set_bool(s->ep_invert_matte, s->invert_matte); while (gs_effect_loop(s->matte_effect, tech_name)) gs_draw_sprite(NULL, 0, cx, cy); gs_enable_framebuffer_srgb(previous); UNUSED_PARAMETER(t); } static void stinger_texrender(struct stinger_info *s, uint32_t source_cx, uint32_t source_cy, uint32_t media_cx, uint32_t media_cy, enum gs_color_space space) { enum gs_color_format format = gs_get_format_from_space(space); if (gs_texrender_get_format(s->stinger_tex) != format) { gs_texrender_destroy(s->stinger_tex); s->stinger_tex = gs_texrender_create(format, GS_ZS_NONE); } if (gs_texrender_begin_with_color_space(s->stinger_tex, source_cx, source_cy, space)) { float cx = (float)media_cx / s->matte_width_factor; float cy = (float)media_cy / s->matte_height_factor; gs_ortho(0.0f, cx, 0.0f, cy, -100.0f, 100.0f); gs_blend_state_push(); gs_enable_blending(false); obs_source_video_render(s->media_source); gs_blend_state_pop(); gs_texrender_end(s->stinger_tex); } } static const char * get_tech_name_and_multiplier(enum gs_color_space current_space, enum gs_color_space source_space, float *multiplier) { const char *tech_name = "Draw"; *multiplier = 1.f; switch (source_space) { case GS_CS_SRGB: case GS_CS_SRGB_16F: if (current_space == GS_CS_709_SCRGB) { tech_name = "DrawMultiply"; *multiplier = obs_get_video_sdr_white_level() / 80.0f; } break; case GS_CS_709_EXTENDED: switch (current_space) { case GS_CS_SRGB: case GS_CS_SRGB_16F: tech_name = "DrawTonemap"; break; case GS_CS_709_SCRGB: tech_name = "DrawMultiply"; *multiplier = obs_get_video_sdr_white_level() / 80.0f; break; case GS_CS_709_EXTENDED: break; } break; case GS_CS_709_SCRGB: switch (current_space) { case GS_CS_SRGB: case GS_CS_SRGB_16F: tech_name = "DrawMultiplyTonemap"; *multiplier = 80.0f / obs_get_video_sdr_white_level(); break; case GS_CS_709_EXTENDED: tech_name = "DrawMultiply"; *multiplier = 80.0f / obs_get_video_sdr_white_level(); break; case GS_CS_709_SCRGB: break; } } return tech_name; } static void stinger_video_render(void *data, gs_effect_t *effect) { struct stinger_info *s = data; uint32_t media_cx = obs_source_get_width(s->media_source); uint32_t media_cy = obs_source_get_height(s->media_source); if (s->track_matte_enabled) { bool ready = obs_source_active(s->media_source) && !!media_cx && !!media_cy; if (ready) { if (!s->matte_rendered) s->matte_rendered = true; obs_transition_video_render(s->source, stinger_matte_render); } else { obs_transition_video_render_direct( s->source, s->matte_rendered ? OBS_TRANSITION_SOURCE_B : OBS_TRANSITION_SOURCE_A); } if (s->matte_layout == MATTE_LAYOUT_MASK) return; } else { float t = obs_transition_get_time(s->source); bool use_a = t < s->transition_point; enum obs_transition_target target = use_a ? OBS_TRANSITION_SOURCE_A : OBS_TRANSITION_SOURCE_B; if (!obs_transition_video_render_direct(s->source, target)) return; } /* --------------------- */ uint32_t source_cx = obs_source_get_width(s->source); uint32_t source_cy = obs_source_get_height(s->source); float source_cxf = (float)source_cx; float source_cyf = (float)source_cy; if (!media_cx || !media_cy) return; if (s->do_texrender) { const enum gs_color_space space = obs_source_get_color_space(s->media_source, 0, NULL); stinger_texrender(s, source_cx, source_cy, media_cx, media_cy, space); const bool previous = gs_framebuffer_srgb_enabled(); gs_enable_framebuffer_srgb(true); float multiplier; const char *technique = get_tech_name_and_multiplier( gs_get_color_space(), space, &multiplier); gs_effect_t *e = obs_get_base_effect(OBS_EFFECT_DEFAULT); gs_eparam_t *p_image = gs_effect_get_param_by_name(e, "image"); gs_eparam_t *p_multiplier = gs_effect_get_param_by_name(e, "multiplier"); gs_texture_t *tex = gs_texrender_get_texture(s->stinger_tex); gs_effect_set_texture_srgb(p_image, tex); gs_effect_set_float(p_multiplier, multiplier); while (gs_effect_loop(e, technique)) gs_draw_sprite(NULL, 0, source_cx, source_cy); gs_enable_framebuffer_srgb(previous); } else { const bool previous = gs_set_linear_srgb(true); gs_matrix_push(); gs_matrix_scale3f(source_cxf / (float)media_cx, source_cyf / (float)media_cy, 1.0f); obs_source_video_render(s->media_source); gs_matrix_pop(); gs_set_linear_srgb(previous); } UNUSED_PARAMETER(effect); } static void stinger_video_tick(void *data, float seconds) { struct stinger_info *s = data; if (s->track_matte_enabled) { gs_texrender_reset(s->stinger_tex); gs_texrender_reset(s->matte_tex); } UNUSED_PARAMETER(seconds); } static inline float calc_fade(float t, float mul) { t *= mul; return t > 1.0f ? 1.0f : t; } static float mix_a_fade_in_out(void *data, float t) { struct stinger_info *s = data; return 1.0f - calc_fade(t, s->transition_a_mul); } static float mix_b_fade_in_out(void *data, float t) { struct stinger_info *s = data; return 1.0f - calc_fade(1.0f - t, s->transition_b_mul); } static float mix_a_cross_fade(void *data, float t) { UNUSED_PARAMETER(data); return 1.0f - t; } static float mix_b_cross_fade(void *data, float t) { UNUSED_PARAMETER(data); return t; } static bool stinger_audio_render(void *data, uint64_t *ts_out, struct obs_source_audio_mix *audio, uint32_t mixers, size_t channels, size_t sample_rate) { struct stinger_info *s = data; uint64_t ts = 0; if (!s) { return false; } if (!obs_source_audio_pending(s->media_source)) { ts = obs_source_get_audio_timestamp(s->media_source); if (!ts) return false; } bool success = obs_transition_audio_render(s->source, ts_out, audio, mixers, channels, sample_rate, s->mix_a, s->mix_b); if (!ts) return success; if (!*ts_out || ts < *ts_out) *ts_out = ts; struct obs_source_audio_mix child_audio; obs_source_get_audio_mix(s->media_source, &child_audio); for (size_t mix = 0; mix < MAX_AUDIO_MIXES; mix++) { if ((mixers & (1 << mix)) == 0) continue; for (size_t ch = 0; ch < channels; ch++) { register float *out = audio->output[mix].data[ch]; register float *in = child_audio.output[mix].data[ch]; register float *end = in + AUDIO_OUTPUT_FRAMES; while (in < end) *(out++) += *(in++); } } return true; } static void stinger_transition_start(void *data) { struct stinger_info *s = data; if (s->media_source) { calldata_t cd = {0}; proc_handler_t *ph = obs_source_get_proc_handler(s->media_source); proc_handler_t *matte_ph = s->matte_source ? obs_source_get_proc_handler(s->matte_source) : NULL; if (s->transitioning) { proc_handler_call(ph, "restart", &cd); if (matte_ph) { proc_handler_call(matte_ph, "restart", &cd); } return; } s->matte_rendered = false; proc_handler_call(ph, "get_duration", &cd); proc_handler_call(ph, "get_nb_frames", &cd); s->duration_ns = (uint64_t)calldata_int(&cd, "duration") + 250000000ULL; s->duration_frames = (uint64_t)calldata_int(&cd, "num_frames"); if (s->transition_point_is_frame) s->transition_point = (float)((long double)s->transition_point_frame / (long double)s->duration_frames); else s->transition_point = (float)((long double)s->transition_point_ns / (long double)s->duration_ns); if (s->transition_point > 0.999f) s->transition_point = 0.999f; else if (s->transition_point < 0.001f) s->transition_point = 0.001f; s->transition_a_mul = (1.0f / s->transition_point); s->transition_b_mul = (1.0f / (1.0f - s->transition_point)); if (s->track_matte_enabled && s->matte_source) { proc_handler_call(matte_ph, "get_duration", &cd); uint64_t tm_duration_ns = (uint64_t)calldata_int(&cd, "duration"); s->duration_ns = ((tm_duration_ns > s->duration_ns) ? (tm_duration_ns) : (s->duration_ns)); obs_source_add_active_child(s->source, s->matte_source); } obs_transition_enable_fixed( s->source, true, (uint32_t)(s->duration_ns / 1000000)); calldata_free(&cd); obs_source_add_active_child(s->source, s->media_source); } s->transitioning = true; } static void stinger_transition_stop(void *data) { struct stinger_info *s = data; if (s->media_source) obs_source_remove_active_child(s->source, s->media_source); if (s->matte_source) obs_source_remove_active_child(s->source, s->matte_source); proc_handler_t *ph = obs_source_get_proc_handler(s->media_source); calldata_t cd = {0}; proc_handler_call(ph, "preload_first_frame", &cd); s->transitioning = false; } static void stinger_enum_active_sources(void *data, obs_source_enum_proc_t enum_callback, void *param) { struct stinger_info *s = data; if (s->media_source && s->transitioning) enum_callback(s->source, s->media_source, param); if (s->matte_source && s->transitioning) enum_callback(s->source, s->matte_source, param); } static void stinger_enum_all_sources(void *data, obs_source_enum_proc_t enum_callback, void *param) { struct stinger_info *s = data; if (s->media_source) enum_callback(s->source, s->media_source, param); if (s->matte_source) enum_callback(s->source, s->matte_source, param); } #define FILE_FILTER " (*.mp4 *.ts *.mov *.wmv *.flv *.mkv *.avi *.gif *.webm);;" static bool transition_point_type_modified(obs_properties_t *ppts, obs_property_t *p, obs_data_t *s) { int64_t type = obs_data_get_int(s, "tp_type"); obs_property_t *prop_transition_point = obs_properties_get(ppts, "transition_point"); if (type == TIMING_TIME) { obs_property_set_description( prop_transition_point, obs_module_text("TransitionPoint")); obs_property_int_set_suffix(prop_transition_point, " ms"); } else { obs_property_set_description( prop_transition_point, obs_module_text("TransitionPointFrame")); obs_property_int_set_suffix(prop_transition_point, ""); } UNUSED_PARAMETER(p); return true; } static bool track_matte_layout_modified(obs_properties_t *ppts, obs_property_t *p, obs_data_t *s) { int matte_layout = (int)obs_data_get_int(s, "track_matte_layout"); obs_property_t *prop_matte_path = obs_properties_get(ppts, "track_matte_path"); bool uses_separate_file = (matte_layout == MATTE_LAYOUT_SEPARATE_FILE); obs_property_set_visible(prop_matte_path, uses_separate_file); UNUSED_PARAMETER(p); return true; } static bool track_matte_enabled_modified(obs_properties_t *ppts, obs_property_t *p, obs_data_t *s) { bool track_matte_enabled = obs_data_get_bool(s, "track_matte_enabled"); obs_property_t *prop_tp_type = obs_properties_get(ppts, "tp_type"); if (track_matte_enabled) { obs_property_set_description( prop_tp_type, obs_module_text("AudioTransitionPointType")); } else { obs_property_set_description( prop_tp_type, obs_module_text("TransitionPointType")); } UNUSED_PARAMETER(p); return true; } static obs_properties_t *stinger_properties(void *data) { obs_properties_t *ppts = obs_properties_create(); struct dstr filter = {0}; obs_properties_set_flags(ppts, OBS_PROPERTIES_DEFER_UPDATE); dstr_copy(&filter, obs_module_text("FileFilter.VideoFiles")); dstr_cat(&filter, FILE_FILTER); dstr_cat(&filter, obs_module_text("FileFilter.AllFiles")); dstr_cat(&filter, " (*.*)"); // main stinger settings obs_properties_add_path(ppts, "path", obs_module_text("VideoFile"), OBS_PATH_FILE, filter.array, NULL); obs_property_t *p = obs_properties_add_list( ppts, "tp_type", obs_module_text("TransitionPointType"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); obs_property_list_add_int(p, obs_module_text("TransitionPointTypeTime"), TIMING_TIME); obs_property_list_add_int( p, obs_module_text("TransitionPointTypeFrame"), TIMING_FRAME); obs_property_set_modified_callback(p, transition_point_type_modified); obs_properties_add_bool(ppts, "hw_decode", obs_module_text("HardwareDecode")); p = obs_properties_add_bool(ppts, "preload", obs_module_text("PreloadVideoToRam")); obs_property_set_long_description( p, obs_module_text("PreloadVideoToRam.Description")); obs_properties_add_int(ppts, "transition_point", obs_module_text("TransitionPoint"), 0, 120000, 1); // track matte properties { obs_properties_t *track_matte_group = obs_properties_create(); p = obs_properties_add_list(track_matte_group, "track_matte_layout", obs_module_text("TrackMatteLayout"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); obs_property_list_add_int( p, obs_module_text("TrackMatteLayoutHorizontal"), MATTE_LAYOUT_HORIZONTAL); obs_property_list_add_int( p, obs_module_text("TrackMatteLayoutVertical"), MATTE_LAYOUT_VERTICAL); /* TODO: Requires way to synchronize or combine two media files * together */ #if 0 obs_property_list_add_int( p, obs_module_text("TrackMatteLayoutSeparateFile"), MATTE_LAYOUT_SEPARATE_FILE); #endif obs_property_list_add_int( p, obs_module_text("TrackMatteLayoutMask"), MATTE_LAYOUT_MASK); obs_property_set_modified_callback(p, track_matte_layout_modified); obs_properties_add_path(track_matte_group, "track_matte_path", obs_module_text("TrackMatteVideoFile"), OBS_PATH_FILE, filter.array, NULL); obs_properties_add_bool(track_matte_group, "invert_matte", obs_module_text("InvertTrackMatte")); p = obs_properties_add_group( ppts, "track_matte_enabled", obs_module_text("TrackMatteEnabled"), OBS_GROUP_CHECKABLE, track_matte_group); obs_property_set_modified_callback( p, track_matte_enabled_modified); } dstr_free(&filter); // audio output settings obs_property_t *monitor_list = obs_properties_add_list( ppts, "audio_monitoring", obs_module_text("AudioMonitoring"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); obs_property_list_add_int(monitor_list, obs_module_text("AudioMonitoring.None"), OBS_MONITORING_TYPE_NONE); obs_property_list_add_int( monitor_list, obs_module_text("AudioMonitoring.MonitorOnly"), OBS_MONITORING_TYPE_MONITOR_ONLY); obs_property_list_add_int(monitor_list, obs_module_text("AudioMonitoring.Both"), OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT); // audio fade settings obs_property_t *audio_fade_style = obs_properties_add_list( ppts, "audio_fade_style", obs_module_text("AudioFadeStyle"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); obs_property_list_add_int( audio_fade_style, obs_module_text("AudioFadeStyle.FadeOutFadeIn"), FADE_STYLE_FADE_OUT_FADE_IN); obs_property_list_add_int(audio_fade_style, obs_module_text("AudioFadeStyle.CrossFade"), FADE_STYLE_CROSS_FADE); UNUSED_PARAMETER(data); return ppts; } static void missing_file_callback(void *src, const char *new_path, void *data) { struct stinger_info *s = src; obs_data_t *settings = obs_source_get_settings(s->source); const char *type = data; if (strcmp(type, "media_source") == 0) { obs_data_set_string(settings, "path", new_path); } else if (strcmp(type, "matte_source") == 0) { obs_data_set_string(settings, "track_matte_path", new_path); } obs_source_update(s->source, settings); obs_data_release(settings); } static obs_missing_files_t *stinger_missing_files(void *data) { struct stinger_info *s = data; obs_data_t *settings = obs_source_get_settings(s->source); obs_missing_files_t *files = obs_missing_files_create(); const char *path = obs_data_get_string(settings, "path"); if (strcmp(path, "") != 0) { if (!os_file_exists(path)) { obs_missing_file_t *file = obs_missing_file_create( path, missing_file_callback, OBS_MISSING_FILE_SOURCE, s->source, (void *)"media_source"); obs_missing_files_add_file(files, file); } } const char *track_matte_path = obs_data_get_string(settings, "track_matte_path"); if (strcmp(track_matte_path, "") != 0) { if (!os_file_exists(track_matte_path)) { obs_missing_file_t *file = obs_missing_file_create( track_matte_path, missing_file_callback, OBS_MISSING_FILE_SOURCE, s->source, (void *)"matte_source"); obs_missing_files_add_file(files, file); } } obs_data_release(settings); return files; } static enum gs_color_space stinger_get_color_space(void *data, size_t count, const enum gs_color_space *preferred_spaces) { UNUSED_PARAMETER(count); UNUSED_PARAMETER(preferred_spaces); struct stinger_info *s = data; return obs_transition_video_get_color_space(s->source); } struct obs_source_info stinger_transition = { .id = "obs_stinger_transition", .type = OBS_SOURCE_TYPE_TRANSITION, .get_name = stinger_get_name, .create = stinger_create, .destroy = stinger_destroy, .update = stinger_update, .get_defaults = stinger_defaults, .video_render = stinger_video_render, .video_tick = stinger_video_tick, .audio_render = stinger_audio_render, .missing_files = stinger_missing_files, .get_properties = stinger_properties, .enum_active_sources = stinger_enum_active_sources, .enum_all_sources = stinger_enum_all_sources, .transition_start = stinger_transition_start, .transition_stop = stinger_transition_stop, .video_get_color_space = stinger_get_color_space, };