diff --git a/plugins/obs-transitions/data/locale/en-US.ini b/plugins/obs-transitions/data/locale/en-US.ini index c3f58bcbb..6af835c83 100644 --- a/plugins/obs-transitions/data/locale/en-US.ini +++ b/plugins/obs-transitions/data/locale/en-US.ini @@ -15,8 +15,16 @@ VideoFile="Video File" TransitionPoint="Transition Point" TransitionPointFrame="Transition Point (frame)" TransitionPointType="Transition Point Type" +AudioTransitionPointType="Audio Transition Point Type" TransitionPointTypeFrame="Frame" TransitionPointTypeTime="Time (milliseconds)" +TrackMatteEnabled="Use a Track Matte" +InvertTrackMatte="Invert Matte Colors" +TrackMatteVideoFile="Track Matte Video File" +TrackMatteLayout="Matte Layout" +TrackMatteLayoutHorizontal="Same file, side-by-side (stinger on the left, track matte on the right)" +TrackMatteLayoutVertical="Same file, stacked (stinger on top, track matte at the bottom)" +TrackMatteLayoutSeparateFile="Separate file (warning: matte can get out of sync)" AudioFadeStyle="Audio Fade Style" AudioFadeStyle.FadeOutFadeIn="Fade out to transition point then fade in" AudioFadeStyle.CrossFade="Crossfade" diff --git a/plugins/obs-transitions/data/stinger_matte_transition.effect b/plugins/obs-transitions/data/stinger_matte_transition.effect new file mode 100644 index 000000000..15d847d81 --- /dev/null +++ b/plugins/obs-transitions/data/stinger_matte_transition.effect @@ -0,0 +1,53 @@ +uniform float4x4 ViewProj; +uniform texture2d a_tex; +uniform texture2d b_tex; +uniform texture2d matte_tex; +uniform bool invert_matte; + +sampler_state textureSampler { + Filter = Linear; + AddressU = Clamp; + AddressV = Clamp; +}; + +struct VertData { + float4 pos : POSITION; + float2 uv : TEXCOORD0; +}; + +VertData VSDefault(VertData v_in) +{ + VertData vert_out; + vert_out.pos = mul(float4(v_in.pos.xyz, 1.0), ViewProj); + vert_out.uv = v_in.uv; + return vert_out; +} + +float4 PSStingerMatte(VertData v_in) : TARGET +{ + float2 uv = v_in.uv; + float4 a_color = a_tex.Sample(textureSampler, uv); + float4 b_color = b_tex.Sample(textureSampler, uv); + float4 matte_color = matte_tex.Sample(textureSampler, uv); + + // RGB -> Luma conversion using Rec. 709 factors + float matte_luma = ( + (matte_color.x * 0.2126) + + (matte_color.y * 0.7152) + + (matte_color.z * 0.0722) + ); + + // if matte invert is enabled, invert the matte color + matte_luma = (invert_matte ? (1.0 - matte_luma) : matte_luma); + + return lerp(a_color, b_color, matte_luma); +} + +technique StingerMatte +{ + pass + { + vertex_shader = VSDefault(v_in); + pixel_shader = PSStingerMatte(v_in); + } +} diff --git a/plugins/obs-transitions/transition-stinger.c b/plugins/obs-transitions/transition-stinger.c index a770f3e50..3cd1e0b27 100644 --- a/plugins/obs-transitions/transition-stinger.c +++ b/plugins/obs-transitions/transition-stinger.c @@ -4,12 +4,17 @@ #define TIMING_TIME 0 #define TIMING_FRAME 1 +#define MATTE_LAYOUT_HORIZONTAL 0 +#define MATTE_LAYOUT_VERTICAL 1 +#define MATTE_LAYOUT_SEPARATE_FILE 2 + 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; @@ -23,6 +28,20 @@ struct stinger_info { 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; + + 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; + float (*mix_a)(void *data, float t); float (*mix_b)(void *data, float t); }; @@ -67,6 +86,36 @@ static void stinger_update(void *data, obs_data_t *settings) else s->transition_point_ns = (uint64_t)(point * 1000000LL); + s->track_matte_enabled = + obs_data_get_bool(settings, "track_matte_enabled"); + s->matte_layout = 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"); + + 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); + + 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); @@ -95,6 +144,33 @@ static void *stinger_create(obs_data_t *settings, obs_source_t *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"); + + s->matte_tex = gs_texrender_create(GS_RGBA, GS_ZS_NONE); + obs_transition_enable_fixed(s->source, true, 0); obs_source_update(source, settings); return s; @@ -104,6 +180,12 @@ static void stinger_destroy(void *data) { struct stinger_info *s = data; obs_source_release(s->media_source); + obs_source_release(s->matte_source); + + gs_texrender_destroy(s->matte_tex); + + gs_effect_destroy(s->matte_effect); + bfree(s); } @@ -112,31 +194,92 @@ 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 + gs_texrender_reset(s->matte_tex); + if (matte_cx > 0 && matte_cy > 0) { + float scale_x = (float)cx / matte_cx; + float scale_y = (float)cy / matte_cy; + + if (gs_texrender_begin(s->matte_tex, cx, cy)) { + gs_matrix_push(); + 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); + obs_source_video_render(matte_source); + gs_matrix_pop(); + + gs_texrender_end(s->matte_tex); + } + } + + gs_effect_set_texture(s->ep_a_tex, a); + gs_effect_set_texture(s->ep_b_tex, b); + 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, "StingerMatte")) + gs_draw_sprite(NULL, 0, cx, cy); + + UNUSED_PARAMETER(t); +} + static void stinger_video_render(void *data, gs_effect_t *effect) { struct stinger_info *s = data; - float t = obs_transition_get_time(s->source); - bool use_a = t < s->transition_point; + if (s->track_matte_enabled) { + obs_transition_video_render(s->source, stinger_matte_render); + } 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; + 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; + if (!obs_transition_video_render_direct(s->source, target)) + return; + } /* --------------------- */ float source_cx = (float)obs_source_get_width(s->source); float source_cy = (float)obs_source_get_height(s->source); + uint32_t media_cx = obs_source_get_width(s->media_source); uint32_t media_cy = obs_source_get_height(s->media_source); if (!media_cx || !media_cy) return; - float scale_x = source_cx / (float)media_cx; - float scale_y = source_cy / (float)media_cy; + float scale_x = source_cx / ((float)media_cx / s->matte_width_factor); + float scale_y = source_cy / ((float)media_cy / s->matte_height_factor); gs_matrix_push(); gs_matrix_scale3f(scale_x, scale_y, 1.0f); @@ -184,6 +327,10 @@ static bool stinger_audio_render(void *data, uint64_t *ts_out, 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) @@ -229,9 +376,14 @@ static void stinger_transition_start(void *data) proc_handler_t *ph = obs_source_get_proc_handler(s->media_source); + proc_handler_t *matte_ph = + obs_source_get_proc_handler(s->matte_source); if (s->transitioning) { proc_handler_call(ph, "restart", &cd); + if (matte_ph) { + proc_handler_call(matte_ph, "restart", &cd); + } return; } @@ -258,6 +410,18 @@ static void stinger_transition_start(void *data) 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) { + 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)); @@ -276,6 +440,9 @@ static void stinger_transition_stop(void *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); + s->transitioning = false; } @@ -286,6 +453,9 @@ static void stinger_enum_active_sources(void *data, 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, @@ -295,6 +465,9 @@ static void stinger_enum_all_sources(void *data, 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 \ @@ -304,17 +477,56 @@ 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"); - p = obs_properties_get(ppts, "transition_point"); + + obs_property_t *prop_transition_point = + obs_properties_get(ppts, "transition_point"); if (type == TIMING_TIME) { obs_property_set_description( - p, obs_module_text("TransitionPoint")); - obs_property_int_set_suffix(p, " ms"); + prop_transition_point, + obs_module_text("TransitionPoint")); } else { obs_property_set_description( - p, obs_module_text("TransitionPointFrame")); - obs_property_int_set_suffix(p, ""); + prop_transition_point, + obs_module_text("TransitionPointFrame")); } + + bool uses_ms_prefix = (type == TIMING_TIME); + obs_property_int_set_suffix(p, (uses_ms_prefix ? " ms" : "")); + + return true; +} + +static bool track_matte_layout_modified(obs_properties_t *ppts, + obs_property_t *p, obs_data_t *s) +{ + int matte_layout = 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; } @@ -324,8 +536,10 @@ static obs_properties_t *stinger_properties(void *data) obs_properties_set_flags(ppts, OBS_PROPERTIES_DEFER_UPDATE); + // main stinger settings obs_properties_add_path(ppts, "path", obs_module_text("VideoFile"), OBS_PATH_FILE, FILE_FILTER, NULL); + obs_property_t *p = obs_properties_add_list( ppts, "tp_type", obs_module_text("TransitionPointType"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); @@ -344,6 +558,45 @@ static obs_properties_t *stinger_properties(void *data) 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); + obs_property_list_add_int( + p, obs_module_text("TrackMatteLayoutSeparateFile"), + MATTE_LAYOUT_SEPARATE_FILE); + + 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, FILE_FILTER, 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); + } + + // 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); @@ -357,6 +610,7 @@ static obs_properties_t *stinger_properties(void *data) 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);