diff --git a/libobs/CMakeLists.txt b/libobs/CMakeLists.txt index 71ae5c6c6..a124c212a 100644 --- a/libobs/CMakeLists.txt +++ b/libobs/CMakeLists.txt @@ -273,6 +273,7 @@ set(libobs_libobs_SOURCES obs-encoder.c obs-service.c obs-source.c + obs-source-transition.c obs-output.c obs-output-delay.c obs.c diff --git a/libobs/obs-internal.h b/libobs/obs-internal.h index 3ff7005e8..960a3f891 100644 --- a/libobs/obs-internal.h +++ b/libobs/obs-internal.h @@ -28,6 +28,7 @@ #include "callback/proc.h" #include "graphics/graphics.h" +#include "graphics/matrix4.h" #include "media-io/audio-resampler.h" #include "media-io/video-io.h" @@ -596,6 +597,27 @@ struct obs_source { uint64_t push_to_mute_stop_time; uint64_t push_to_talk_delay; uint64_t push_to_talk_stop_time; + + /* transitions */ + uint64_t transition_start_time; + uint64_t transition_duration; + pthread_mutex_t transition_tex_mutex; + gs_texrender_t *transition_texrender[2]; + pthread_mutex_t transition_mutex; + obs_source_t *transition_sources[2]; + bool transitioning_video; + bool transitioning_audio; + bool transition_source_active[2]; + uint32_t transition_alignment; + uint32_t transition_actual_cx; + uint32_t transition_actual_cy; + uint32_t transition_cx; + uint32_t transition_cy; + uint32_t transition_fixed_duration; + bool transition_use_fixed_duration : 1; + enum obs_transition_mode transition_mode; + enum obs_transition_scale_type transition_scale_type; + struct matrix4 transition_matrices[2]; }; extern const struct obs_source_info *get_source_info(const char *id); @@ -606,6 +628,14 @@ extern bool obs_source_init_context(struct obs_source *source, extern void obs_source_save(obs_source_t *source); extern void obs_source_load(obs_source_t *source); +extern bool obs_transition_init(obs_source_t *transition); +extern void obs_transition_free(obs_source_t *transition); +extern void obs_transition_tick(obs_source_t *transition); +extern void obs_transition_enum_sources(obs_source_t *transition, + obs_source_enum_proc_t enum_callback, void *param); +extern void obs_transition_save(obs_source_t *source, obs_data_t *data); +extern void obs_transition_load(obs_source_t *source, obs_data_t *data); + extern void obs_source_destroy(struct obs_source *source); enum view_type { diff --git a/libobs/obs-source-transition.c b/libobs/obs-source-transition.c new file mode 100644 index 000000000..b4b74c4ff --- /dev/null +++ b/libobs/obs-source-transition.c @@ -0,0 +1,935 @@ +/****************************************************************************** + Copyright (C) 2013-2014 by Hugh Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "obs-internal.h" + +#define lock_transition(transition) \ + pthread_mutex_lock(&transition->transition_mutex); +#define unlock_transition(transition) \ + pthread_mutex_unlock(&transition->transition_mutex); + +#define trylock_textures(transition) \ + pthread_mutex_trylock(&transition->transition_tex_mutex) +#define lock_textures(transition) \ + pthread_mutex_lock(&transition->transition_tex_mutex) +#define unlock_textures(transition) \ + pthread_mutex_unlock(&transition->transition_tex_mutex) + +static inline bool transition_valid(const obs_source_t *transition, + const char *func) +{ + if (!obs_ptr_valid(transition, func)) + return false; + else if (transition->info.type != OBS_SOURCE_TYPE_TRANSITION) + return false; + + return true; +} + +bool obs_transition_init(obs_source_t *transition) +{ + pthread_mutex_init_value(&transition->transition_mutex); + pthread_mutex_init_value(&transition->transition_tex_mutex); + if (pthread_mutex_init(&transition->transition_mutex, NULL) != 0) + return false; + if (pthread_mutex_init(&transition->transition_tex_mutex, NULL) != 0) + return false; + + transition->transition_alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP; + transition->transition_texrender[0] = + gs_texrender_create(GS_RGBA, GS_ZS_NONE); + transition->transition_texrender[1] = + gs_texrender_create(GS_RGBA, GS_ZS_NONE); + transition->transition_source_active[0] = true; + + return transition->transition_texrender[0] != NULL && + transition->transition_texrender[1] != NULL; +} + +void obs_transition_free(obs_source_t *transition) +{ + pthread_mutex_destroy(&transition->transition_mutex); + pthread_mutex_destroy(&transition->transition_tex_mutex); + + gs_enter_context(obs->video.graphics); + gs_texrender_destroy(transition->transition_texrender[0]); + gs_texrender_destroy(transition->transition_texrender[1]); + gs_leave_context(); +} + +void obs_transition_clear(obs_source_t *transition) +{ + obs_source_t *s[2]; + bool active[2]; + + if (!transition_valid(transition, "obs_transition_clear")) + return; + + lock_transition(transition); + for (size_t i = 0; i < 2; i++) { + s[i] = transition->transition_sources[i]; + active[i] = transition->transition_source_active[i]; + transition->transition_sources[i] = NULL; + transition->transition_source_active[i] = false; + } + transition->transitioning_video = false; + transition->transitioning_audio = false; + unlock_transition(transition); + + for (size_t i = 0; i < 2; i++) { + if (s[i] && active[i]) + obs_source_remove_active_child(transition, s[i]); + obs_source_release(s[i]); + } +} + +void add_alignment(struct vec2 *v, uint32_t align, int cx, int cy); + +static void recalculate_transition_matrix(obs_source_t *tr, size_t idx) +{ + obs_source_t *child; + struct matrix4 mat; + struct vec2 pos; + struct vec2 scale; + float tr_cx = (float)tr->transition_actual_cx; + float tr_cy = (float)tr->transition_actual_cy; + float source_cx; + float source_cy; + float tr_aspect = tr_cx / tr_cy; + float source_aspect; + enum obs_transition_scale_type scale_type = tr->transition_scale_type; + + lock_transition(tr); + + child = tr->transition_sources[idx]; + if (!child) { + unlock_transition(tr); + return; + } + + source_cx = (float)obs_source_get_width(child); + source_cy = (float)obs_source_get_height(child); + unlock_transition(tr); + + if (source_cx == 0.0f || source_cy == 0.0f) + return; + + source_aspect = source_cx / source_cy; + + if (scale_type == OBS_TRANSITION_SCALE_MAX_ONLY) { + if (source_cx > tr_cx || source_cy > tr_cy) { + scale_type = OBS_TRANSITION_SCALE_ASPECT; + } else { + scale.x = 1.0f; + scale.y = 1.0f; + } + } + + if (scale_type == OBS_TRANSITION_SCALE_ASPECT) { + bool use_width = tr_aspect < source_aspect; + scale.x = scale.y = use_width ? + tr_cx / source_cx : + tr_cy / source_cy; + + } else if (scale_type == OBS_TRANSITION_SCALE_STRETCH) { + scale.x = tr_cx / source_cx; + scale.y = tr_cy / source_cy; + } + + source_cx *= scale.x; + source_cy *= scale.y; + + vec2_zero(&pos); + add_alignment(&pos, tr->transition_alignment, + (int)(tr_cx - source_cx), + (int)(tr_cy - source_cy)); + + matrix4_identity(&mat); + matrix4_scale3f(&mat, &mat, scale.x, scale.y, 1.0f); + matrix4_translate3f(&mat, &mat, pos.x, pos.y, 0.0f); + matrix4_copy(&tr->transition_matrices[idx], &mat); +} + +static inline void recalculate_transition_matrices(obs_source_t *transition) +{ + recalculate_transition_matrix(transition, 0); + recalculate_transition_matrix(transition, 1); +} + +static void recalculate_transition_size(obs_source_t *transition) +{ + uint32_t cx = 0, cy = 0; + obs_source_t *child; + + lock_transition(transition); + + for (size_t i = 0; i < 2; i++) { + child = transition->transition_sources[i]; + if (child) { + uint32_t new_cx = obs_source_get_width(child); + uint32_t new_cy = obs_source_get_height(child); + if (new_cx > cx) cx = new_cx; + if (new_cy > cy) cy = new_cy; + } + } + + unlock_transition(transition); + + transition->transition_actual_cx = cx; + transition->transition_actual_cy = cy; +} + +void obs_transition_tick(obs_source_t *transition) +{ + recalculate_transition_size(transition); + recalculate_transition_matrices(transition); + + if (trylock_textures(transition) == 0) { + gs_texrender_reset(transition->transition_texrender[0]); + gs_texrender_reset(transition->transition_texrender[1]); + unlock_textures(transition); + } +} + +static void set_source(obs_source_t *transition, + enum obs_transition_target target, obs_source_t *new_child, + bool (*callback)(obs_source_t *t, size_t idx, obs_source_t *c)) +{ + size_t idx = (size_t)target; + obs_source_t *old_child; + bool add_success = true; + bool already_active; + + if (new_child) + obs_source_addref(new_child); + + lock_transition(transition); + + old_child = transition->transition_sources[idx]; + + if (new_child == old_child) { + unlock_transition(transition); + obs_source_release(new_child); + return; + } + + already_active = transition->transition_source_active[idx]; + + if (already_active) { + if (new_child) + add_success = obs_source_add_active_child(transition, + new_child); + if (old_child && add_success) + obs_source_remove_active_child(transition, old_child); + } + + if (callback && add_success) + add_success = callback(transition, idx, new_child); + + transition->transition_sources[idx] = add_success ? new_child : NULL; + + unlock_transition(transition); + + if (add_success) { + if (transition->transition_cx == 0 || + transition->transition_cy == 0) { + recalculate_transition_size(transition); + recalculate_transition_matrices(transition); + } + } else { + obs_source_release(new_child); + } + + obs_source_release(old_child); +} + +obs_source_t *obs_transition_get_source(obs_source_t *transition, + enum obs_transition_target target) +{ + size_t idx = (size_t)target; + obs_source_t *ret; + + if (!transition_valid(transition, "obs_transition_get_source")) + return NULL; + + lock_transition(transition); + ret = transition->transition_sources[idx]; + obs_source_addref(ret); + unlock_transition(transition); + + return ret; +} + +obs_source_t *obs_transition_get_active_source(obs_source_t *transition) +{ + obs_source_t *ret; + + if (!transition_valid(transition, "obs_transition_get_source")) + return NULL; + + lock_transition(transition); + if (transition->transitioning_audio || transition->transitioning_video) + ret = transition->transition_sources[1]; + else + ret = transition->transition_sources[0]; + obs_source_addref(ret); + unlock_transition(transition); + + return ret; +} + +static inline bool activate_child(obs_source_t *transition, size_t idx) +{ + bool success = true; + + lock_transition(transition); + + if (transition->transition_sources[idx] && + !transition->transition_source_active[idx]) { + + success = obs_source_add_active_child(transition, + transition->transition_sources[idx]); + if (success) + transition->transition_source_active[idx] = true; + } + + unlock_transition(transition); + + return success; +} + +static bool activate_transition(obs_source_t *transition, size_t idx, + obs_source_t *child) +{ + if (!transition->transition_source_active[idx]) { + if (!obs_source_add_active_child(transition, child)) + return false; + + transition->transition_source_active[idx] = true; + } + + transition->transitioning_video = true; + transition->transitioning_audio = true; + return true; +} + +static inline bool transition_active(obs_source_t *transition) +{ + return transition->transitioning_audio || + transition->transitioning_video; +} + +bool obs_transition_start(obs_source_t *transition, + enum obs_transition_mode mode, uint32_t duration_ms, + obs_source_t *dest) +{ + bool active; + bool same_as_source; + bool same_as_dest; + + if (!transition_valid(transition, "obs_transition_start")) + return false; + + lock_transition(transition); + same_as_source = dest == transition->transition_sources[0]; + same_as_dest = dest == transition->transition_sources[1]; + active = transition_active(transition); + unlock_transition(transition); + + if (same_as_source && !active) + return false; + + if (transition->transition_use_fixed_duration) + duration_ms = transition->transition_fixed_duration; + + if (!active || (!same_as_dest && !same_as_source)) { + transition->transition_start_time = os_gettime_ns(); + transition->transition_duration = + (uint64_t)duration_ms * 1000000ULL; + } + + set_source(transition, OBS_TRANSITION_SOURCE_B, dest, + activate_transition); + + obs_source_dosignal(transition, "source_transition_start", + "transition_start"); + + /* TODO: Add mode */ + UNUSED_PARAMETER(mode); + return true; +} + +void obs_transition_set(obs_source_t *transition, obs_source_t *source) +{ + obs_source_t *s[2]; + bool active[2]; + + if (!transition_valid(transition, "obs_transition_clear")) + return; + + obs_source_addref(source); + + lock_transition(transition); + for (size_t i = 0; i < 2; i++) { + s[i] = transition->transition_sources[i]; + active[i] = transition->transition_source_active[i]; + transition->transition_sources[i] = NULL; + transition->transition_source_active[i] = false; + } + transition->transition_source_active[0] = true; + transition->transition_sources[0] = source; + transition->transitioning_video = false; + transition->transitioning_audio = false; + unlock_transition(transition); + + for (size_t i = 0; i < 2; i++) { + if (s[i] && active[i]) + obs_source_remove_active_child(transition, s[i]); + obs_source_release(s[i]); + } + + if (source) + obs_source_add_active_child(transition, source); +} + +static float calc_time(obs_source_t *transition, uint64_t ts) +{ + uint64_t end; + + if (ts <= transition->transition_start_time) + return 0.0f; + + end = transition->transition_duration; + ts -= transition->transition_start_time; + if (ts >= end || end == 0) + return 1.0f; + + return (float)((long double)ts / (long double)end); +} + +static inline float get_video_time(obs_source_t *transition) +{ + uint64_t ts = obs->video.video_time; + return calc_time(transition, ts); +} + +static inline gs_texture_t *get_texture(obs_source_t *transition, + enum obs_transition_target target) +{ + size_t idx = (size_t)target; + return gs_texrender_get_texture(transition->transition_texrender[idx]); +} + +void obs_transition_set_scale_type(obs_source_t *transition, + enum obs_transition_scale_type type) +{ + if (!transition_valid(transition, "obs_transition_set_scale_type")) + return; + + transition->transition_scale_type = type; +} + +enum obs_transition_scale_type obs_transition_get_scale_type( + const obs_source_t *transition) +{ + return transition_valid(transition, "obs_transition_get_scale_type") ? + transition->transition_scale_type : + OBS_TRANSITION_SCALE_MAX_ONLY; +} + +void obs_transition_set_alignment(obs_source_t *transition, uint32_t alignment) +{ + if (!transition_valid(transition, "obs_transition_set_alignment")) + return; + + transition->transition_alignment = alignment; +} + +uint32_t obs_transition_get_alignment(const obs_source_t *transition) +{ + return transition_valid(transition, "obs_transition_get_alignment") ? + transition->transition_alignment : 0; +} + +void obs_transition_set_size(obs_source_t *transition, + uint32_t cx, uint32_t cy) +{ + if (!transition_valid(transition, "obs_transition_set_size")) + return; + + transition->transition_cx = cx; + transition->transition_cy = cy; +} + +void obs_transition_get_size(const obs_source_t *transition, + uint32_t *cx, uint32_t *cy) +{ + if (!transition_valid(transition, "obs_transition_set_size")) { + *cx = 0; + *cy = 0; + return; + } + + *cx = transition->transition_cx; + *cy = transition->transition_cy; +} + +void obs_transition_save(obs_source_t *tr, obs_data_t *data) +{ + obs_source_t *child; + + lock_transition(tr); + child = transition_active(tr) ? + tr->transition_sources[1] : tr->transition_sources[0]; + + obs_data_set_string(data, "transition_source_a", + child ? child->context.name : ""); + obs_data_set_int(data, "transition_alignment", + tr->transition_alignment); + obs_data_set_int(data, "transition_mode", + (int64_t)tr->transition_mode); + obs_data_set_int(data, "transition_scale_type", + (int64_t)tr->transition_scale_type); + obs_data_set_int(data, "transition_cx", tr->transition_cx); + obs_data_set_int(data, "transition_cy", tr->transition_cy); + unlock_transition(tr); +} + +void obs_transition_load(obs_source_t *tr, obs_data_t *data) +{ + const char *name = obs_data_get_string(data, "transition_source_a"); + int64_t alignment = obs_data_get_int(data, "transition_alignment"); + int64_t mode = obs_data_get_int(data, "transition_mode"); + int64_t scale_type = obs_data_get_int(data, "transition_scale_type"); + int64_t cx = obs_data_get_int(data, "transition_cx"); + int64_t cy = obs_data_get_int(data, "transition_cy"); + obs_source_t *source = NULL; + + if (name) { + source = obs_get_source_by_name(name); + if (source) { + if (!obs_source_add_active_child(tr, source)) { + blog(LOG_WARNING, "Cannot set transition '%s' " + "to source '%s' due to " + "infinite recursion", + tr->context.name, name); + obs_source_release(source); + source = NULL; + } + } else { + blog(LOG_WARNING, "Failed to find source '%s' for " + "transition '%s'", + name, tr->context.name); + } + } + + lock_transition(tr); + tr->transition_sources[0] = source; + tr->transition_source_active[0] = true; + tr->transition_alignment = (uint32_t)alignment; + tr->transition_mode = (enum obs_transition_mode)mode; + tr->transition_scale_type = (enum obs_transition_scale_type)scale_type; + tr->transition_cx = (uint32_t)cx; + tr->transition_cy = (uint32_t)cy; + unlock_transition(tr); + + recalculate_transition_size(tr); + recalculate_transition_matrices(tr); +} + +struct transition_state { + obs_source_t *s[2]; + bool transitioning_video; + bool transitioning_audio; +}; + +static inline void copy_transition_state(obs_source_t *transition, + struct transition_state *state) +{ + state->s[0] = transition->transition_sources[0]; + state->s[1] = transition->transition_sources[1]; + obs_source_addref(state->s[0]); + obs_source_addref(state->s[1]); + + state->transitioning_video = transition->transitioning_video; + state->transitioning_audio = transition->transitioning_audio; +} + +static inline void enum_child(obs_source_t *tr, obs_source_t *child, + obs_source_enum_proc_t enum_callback, void *param) +{ + if (!child) + return; + + if (child->context.data && child->info.enum_active_sources) + child->info.enum_active_sources(child->context.data, + enum_callback, param); + + enum_callback(tr, child, param); +} + +void obs_transition_enum_sources(obs_source_t *transition, + obs_source_enum_proc_t cb, void *param) +{ + lock_transition(transition); + for (size_t i = 0; i < 2; i++) { + if (transition->transition_sources[i]) + cb(transition, transition->transition_sources[i], param); + } + unlock_transition(transition); +} + +static inline void render_child(obs_source_t *transition, + obs_source_t *child, size_t idx) +{ + uint32_t cx = transition->transition_actual_cx; + uint32_t cy = transition->transition_actual_cy; + struct vec4 blank; + if (!child) + return; + + if (gs_texrender_begin(transition->transition_texrender[idx], cx, cy)) { + vec4_zero(&blank); + gs_clear(GS_CLEAR_COLOR, &blank, 0.0f, 0); + + gs_matrix_push(); + gs_matrix_mul(&transition->transition_matrices[idx]); + obs_source_video_render(child); + gs_matrix_pop(); + + gs_texrender_end(transition->transition_texrender[idx]); + } +} + +static void obs_transition_stop(obs_source_t *transition) +{ + obs_source_t *old_child = transition->transition_sources[0]; + + if (old_child && transition->transition_source_active[0]) + obs_source_remove_active_child(transition, old_child); + obs_source_release(old_child); + + transition->transition_source_active[0] = true; + transition->transition_source_active[1] = false; + transition->transition_sources[0] = transition->transition_sources[1]; + transition->transition_sources[1] = NULL; +} + +void obs_transition_video_render(obs_source_t *transition, + obs_transition_video_render_callback_t callback) +{ + struct transition_state state; + bool locked = false; + bool stopped = false; + bool video_stopped = false; + float t; + + if (!transition_valid(transition, "obs_transition_video_render")) + return; + + t = get_video_time(transition); + + lock_transition(transition); + + if (t >= 1.0f && transition->transitioning_video) { + transition->transitioning_video = false; + video_stopped = true; + + if (!transition->transitioning_audio) { + obs_transition_stop(transition); + stopped = true; + } + } + copy_transition_state(transition, &state); + + unlock_transition(transition); + + if (state.transitioning_video) + locked = trylock_textures(transition) == 0; + + if (state.transitioning_video && locked && callback) { + gs_texture_t *tex[2]; + + for (size_t i = 0; i < 2; i++) { + if (state.s[i]) { + render_child(transition, state.s[i], i); + tex[i] = get_texture(transition, i); + if (!tex[i]) + tex[i] = obs->video.transparent_texture; + } else { + tex[i] = obs->video.transparent_texture; + } + } + + callback(transition->context.data, tex[0], tex[1], t, + transition->transition_actual_cx, + transition->transition_actual_cy); + + } else if (state.transitioning_audio) { + if (state.s[1]) + obs_source_video_render(state.s[1]); + + } else { + if (state.s[0]) + obs_source_video_render(state.s[0]); + } + + if (locked) + unlock_textures(transition); + + obs_source_release(state.s[0]); + obs_source_release(state.s[1]); + + if (video_stopped) + obs_source_dosignal(transition, "source_transition_video_stop", + "transition_video_stop"); + if (stopped) + obs_source_dosignal(transition, "source_transition_stop", + "transition_stop"); +} + +static inline float get_sample_time(obs_source_t *transition, + size_t sample_rate, size_t sample, uint64_t ts) +{ + uint64_t sample_ts_offset = (uint64_t)sample * 1000000000ULL / + (uint64_t)sample_rate; + uint64_t i_ts = ts + sample_ts_offset; + return calc_time(transition, i_ts); +} + +static inline void mix_child(obs_source_t *transition, float *out, float *in, + size_t count, size_t sample_rate, uint64_t ts, + obs_transition_audio_mix_callback_t mix) +{ + void *context_data = transition->context.data; + + for (size_t i = 0; i < count; i++) { + float t = get_sample_time(transition, sample_rate, i, ts); + out[i] += in[i] * mix(context_data, t); + } +} + +static void process_audio(obs_source_t *transition, obs_source_t *child, + struct obs_source_audio_mix *audio, uint64_t min_ts, + uint32_t mixers, size_t channels, size_t sample_rate, + obs_transition_audio_mix_callback_t mix) +{ + bool valid = child && !child->audio_pending; + struct obs_source_audio_mix child_audio; + uint64_t ts; + size_t pos; + + if (!valid) + return; + + ts = child->audio_ts; + obs_source_get_audio_mix(child, &child_audio); + + for (size_t mix_idx = 0; mix_idx < MAX_AUDIO_MIXES; mix_idx++) { + struct audio_output_data *output = &audio->output[mix_idx]; + struct audio_output_data *input = &child_audio.output[mix_idx]; + + if ((mixers & (1 << mix_idx)) == 0) + continue; + + pos = (size_t)ns_to_audio_frames(sample_rate, ts - min_ts); + + for (size_t ch = 0; ch < channels; ch++) { + float *out = output->data[ch]; + float *in = input->data[ch]; + + mix_child(transition, out + pos, in, + AUDIO_OUTPUT_FRAMES - pos, + sample_rate, ts, mix); + } + } +} + +static inline uint64_t calc_min_ts(obs_source_t *sources[2]) +{ + uint64_t min_ts = 0; + + for (size_t i = 0; i < 2; i++) { + if (sources[i] && !sources[i]->audio_pending) { + if (!min_ts || sources[i]->audio_ts < min_ts) + min_ts = sources[i]->audio_ts; + } + } + + return min_ts; +} + +#define TOTAL_AUDIO_SIZE \ + (MAX_AUDIO_MIXES * MAX_AUDIO_CHANNELS * \ + AUDIO_OUTPUT_FRAMES * sizeof(float)) + +static inline bool stop_audio(obs_source_t *transition) +{ + transition->transitioning_audio = false; + if (!transition->transitioning_video) { + obs_transition_stop(transition); + return true; + } + + return false; +} + +bool obs_transition_audio_render(obs_source_t *transition, + uint64_t *ts_out, struct obs_source_audio_mix *audio, + uint32_t mixers, size_t channels, size_t sample_rate, + obs_transition_audio_mix_callback_t mix_a, + obs_transition_audio_mix_callback_t mix_b) +{ + obs_source_t *sources[2]; + struct transition_state state = {0}; + bool stopped = false; + uint64_t min_ts; + float t; + + if (!transition_valid(transition, "obs_transition_audio_render")) + return false; + + lock_transition(transition); + + sources[0] = transition->transition_sources[0]; + sources[1] = transition->transition_sources[1]; + + min_ts = calc_min_ts(sources); + + if (min_ts) { + t = calc_time(transition, min_ts); + + if (t >= 1.0f && transition->transitioning_audio) + stopped = stop_audio(transition); + + sources[0] = transition->transition_sources[0]; + sources[1] = transition->transition_sources[1]; + min_ts = calc_min_ts(sources); + if (min_ts) + copy_transition_state(transition, &state); + + } else if (transition->transitioning_audio) { + stopped = stop_audio(transition); + } + + unlock_transition(transition); + + if (min_ts) { + if (state.transitioning_audio) { + if (state.s[0]) + process_audio(transition, state.s[0], audio, + min_ts, mixers, channels, + sample_rate, mix_a); + if (state.s[1]) + process_audio(transition, state.s[1], audio, + min_ts, mixers, channels, + sample_rate, mix_b); + } else if (state.s[0]) { + memcpy(audio->output[0].data[0], + state.s[0]->audio_output_buf[0][0], + TOTAL_AUDIO_SIZE); + } + + obs_source_release(state.s[0]); + obs_source_release(state.s[1]); + } + + if (stopped) + obs_source_dosignal(transition, "source_transition_stop", + "transition_stop"); + + *ts_out = min_ts; + return !!min_ts; +} + +void obs_transition_enable_fixed(obs_source_t *transition, + bool enable, uint32_t duration) +{ + if (!transition_valid(transition, "obs_transition_enable_fixed")) + return; + + transition->transition_use_fixed_duration = enable; + transition->transition_fixed_duration = duration; +} + +bool obs_transition_fixed(obs_source_t *transition) +{ + return transition_valid(transition, "obs_transition_fixed") ? + transition->transition_use_fixed_duration : false; +} + +static inline obs_source_t *copy_source_state(obs_source_t *tr_dest, + obs_source_t *tr_source, size_t idx) +{ + obs_source_t *old_child = tr_dest->transition_sources[idx]; + obs_source_t *new_child = tr_source->transition_sources[idx]; + bool active = tr_source->transition_source_active[idx]; + + if (old_child && tr_dest->transition_source_active[idx]) + obs_source_remove_active_child(tr_dest, old_child); + + tr_dest->transition_sources[idx] = new_child; + tr_dest->transition_source_active[idx] = active; + + if (active && new_child) + obs_source_add_active_child(tr_dest, new_child); + obs_source_addref(new_child); + + return old_child; +} + +void obs_transition_swap_begin(obs_source_t *tr_dest, obs_source_t *tr_source) +{ + obs_source_t *old_children[2]; + + if (tr_dest == tr_source) + return; + + lock_textures(tr_source); + lock_textures(tr_dest); + + lock_transition(tr_source); + lock_transition(tr_dest); + + for (size_t i = 0; i < 2; i++) + old_children[i] = copy_source_state(tr_dest, tr_source, i); + + unlock_transition(tr_dest); + unlock_transition(tr_source); + + for (size_t i = 0; i < 2; i++) + obs_source_release(old_children[i]); +} + +void obs_transition_swap_end(obs_source_t *tr_dest, obs_source_t *tr_source) +{ + if (tr_dest == tr_source) + return; + + obs_transition_clear(tr_source); + + for (size_t i = 0; i < 2; i++) { + gs_texrender_t *dest = tr_dest->transition_texrender[i]; + gs_texrender_t *source = tr_source->transition_texrender[i]; + + tr_dest->transition_texrender[i] = source; + tr_source->transition_texrender[i] = dest; + } + + unlock_textures(tr_dest); + unlock_textures(tr_source); +} diff --git a/libobs/obs-source.c b/libobs/obs-source.c index ee321aae6..fb017229b 100644 --- a/libobs/obs-source.c +++ b/libobs/obs-source.c @@ -70,6 +70,9 @@ static const char *source_signals[] = { "void filter_add(ptr source, ptr filter)", "void filter_remove(ptr source, ptr filter)", "void reorder_filters(ptr source)", + "void transition_start(ptr source)", + "void transition_video_stop(ptr source)", + "void transition_stop(ptr source)", NULL }; @@ -161,6 +164,11 @@ bool obs_source_init(struct obs_source *source) pthread_mutex_unlock(&obs->data.audio_sources_mutex); } + if (source->info.type == OBS_SOURCE_TYPE_TRANSITION) { + if (!obs_transition_init(source)) + return false; + } + source->control = bzalloc(sizeof(obs_weak_source_t)); source->control->source = source; source->audio_mixers = 0xF; @@ -351,6 +359,9 @@ void obs_source_destroy(struct obs_source *source) if (!obs_source_valid(source, "obs_source_destroy")) return; + if (source->info.type == OBS_SOURCE_TYPE_TRANSITION) + obs_transition_clear(source); + pthread_mutex_lock(&obs->data.audio_sources_mutex); if (source->prev_next_audio_source) { *source->prev_next_audio_source = source->next_audio_source; @@ -400,6 +411,9 @@ void obs_source_destroy(struct obs_source *source) audio_resampler_destroy(source->resampler); bfree(source->audio_output_buf[0][0]); + if (source->info.type == OBS_SOURCE_TYPE_TRANSITION) + obs_transition_free(source); + da_free(source->audio_actions); da_free(source->async_cache); da_free(source->async_frames); @@ -793,6 +807,9 @@ void obs_source_video_tick(obs_source_t *source, float seconds) if (!obs_source_valid(source, "obs_source_video_tick")) return; + if (source->info.type == OBS_SOURCE_TYPE_TRANSITION) + obs_transition_tick(source); + if ((source->info.output_flags & OBS_SOURCE_ASYNC) != 0) { uint64_t sys_time = obs->video.video_time; @@ -1548,7 +1565,10 @@ static uint32_t get_base_width(const obs_source_t *source) { bool is_filter = (source->info.type == OBS_SOURCE_TYPE_FILTER); - if (source->info.get_width && (!is_filter || source->enabled)) { + if (source->info.type == OBS_SOURCE_TYPE_TRANSITION) { + return source->enabled ? source->transition_actual_cx : 0; + + } else if (source->info.get_width && (!is_filter || source->enabled)) { return source->info.get_width(source->context.data); } else if (source->info.type == OBS_SOURCE_TYPE_FILTER) { @@ -1562,7 +1582,10 @@ static uint32_t get_base_height(const obs_source_t *source) { bool is_filter = (source->info.type == OBS_SOURCE_TYPE_FILTER); - if (source->info.get_height && (!is_filter || source->enabled)) { + if (source->info.type == OBS_SOURCE_TYPE_TRANSITION) { + return source->enabled ? source->transition_actual_cy : 0; + + } else if (source->info.get_height && (!is_filter || source->enabled)) { return source->info.get_height(source->context.data); } else if (is_filter) { @@ -2703,15 +2726,21 @@ void obs_source_enum_active_sources(obs_source_t *source, obs_source_enum_proc_t enum_callback, void *param) { + bool is_transition; if (!data_valid(source, "obs_source_enum_active_sources")) return; - if (!source->info.enum_active_sources) + + is_transition = source->info.type == OBS_SOURCE_TYPE_TRANSITION; + if (!is_transition && !source->info.enum_active_sources) return; obs_source_addref(source); - source->info.enum_active_sources(source->context.data, enum_callback, - param); + if (is_transition) + obs_transition_enum_sources(source, enum_callback, param); + if (source->info.enum_active_sources) + source->info.enum_active_sources(source->context.data, + enum_callback, param); obs_source_release(source); } @@ -2721,17 +2750,23 @@ void obs_source_enum_active_tree(obs_source_t *source, void *param) { struct source_enum_data data = {enum_callback, param}; + bool is_transition; if (!data_valid(source, "obs_source_enum_active_tree")) return; - if (!source->info.enum_active_sources) + + is_transition = source->info.type == OBS_SOURCE_TYPE_TRANSITION; + if (!is_transition && !source->info.enum_active_sources) return; obs_source_addref(source); - source->info.enum_active_sources(source->context.data, - enum_source_tree_callback, - &data); + if (source->info.type == OBS_SOURCE_TYPE_TRANSITION) + obs_transition_enum_sources(source, enum_source_tree_callback, + &data); + if (source->info.enum_active_sources) + source->info.enum_active_sources(source->context.data, + enum_source_tree_callback, &data); obs_source_release(source); } diff --git a/libobs/obs.c b/libobs/obs.c index 6615b0a06..a8911e486 100644 --- a/libobs/obs.c +++ b/libobs/obs.c @@ -587,6 +587,9 @@ static const char *obs_signals[] = { "void source_volume(ptr source, in out float volume)", "void source_volume_level(ptr source, float level, float magnitude, " "float peak)", + "void source_transition_start(ptr source)", + "void source_transition_video_stop(ptr source)", + "void source_transition_stop(ptr source)", "void channel_change(int channel, in out ptr source, ptr prev_source)", "void master_volume(in out float volume)", @@ -1508,8 +1511,17 @@ void obs_load_sources(obs_data_array_t *array) } /* tell sources that we want to load */ - for (i = 0; i < sources.num; i++) - obs_source_load(sources.array[i]); + for (i = 0; i < sources.num; i++) { + obs_source_t *source = sources.array[i]; + obs_data_t *source_data = obs_data_array_item(array, i); + if (source) { + if (source->info.type == OBS_SOURCE_TYPE_TRANSITION) + obs_transition_load(source, source_data); + obs_source_load(source); + } + obs_data_release(source_data); + } + for (i = 0; i < sources.num; i++) obs_source_release(sources.array[i]); @@ -1562,6 +1574,9 @@ obs_data_t *obs_save_source(obs_source_t *source) obs_data_set_int (source_data, "push-to-talk-delay", ptt_delay); obs_data_set_obj (source_data, "hotkeys", hotkey_data); + if (source->info.type == OBS_SOURCE_TYPE_TRANSITION) + obs_transition_save(source, source_data); + pthread_mutex_lock(&source->filter_mutex); if (source->filters.num) { diff --git a/libobs/obs.h b/libobs/obs.h index 8bbbcf047..9a6ebdf7b 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -976,6 +976,80 @@ EXPORT uint64_t obs_source_get_audio_timestamp(const obs_source_t *source); EXPORT void obs_source_get_audio_mix(const obs_source_t *source, struct obs_source_audio_mix *audio); +/* ------------------------------------------------------------------------- */ +/* Transition-specific functions */ +enum obs_transition_target { + OBS_TRANSITION_SOURCE_A, + OBS_TRANSITION_SOURCE_B +}; + +EXPORT obs_source_t *obs_transition_get_source(obs_source_t *transition, + enum obs_transition_target target); +EXPORT void obs_transition_clear(obs_source_t *transition); + +EXPORT obs_source_t *obs_transition_get_active_source(obs_source_t *transition); + +enum obs_transition_mode { + OBS_TRANSITION_MODE_AUTO, +}; + +EXPORT bool obs_transition_start(obs_source_t *transition, + enum obs_transition_mode mode, uint32_t duration_ms, + obs_source_t *dest); + +EXPORT void obs_transition_set(obs_source_t *transition, obs_source_t *source); + +enum obs_transition_scale_type { + OBS_TRANSITION_SCALE_MAX_ONLY, + OBS_TRANSITION_SCALE_ASPECT, + OBS_TRANSITION_SCALE_STRETCH, +}; + +EXPORT void obs_transition_set_scale_type(obs_source_t *transition, + enum obs_transition_scale_type type); +EXPORT enum obs_transition_scale_type obs_transition_get_scale_type( + const obs_source_t *transition); + +EXPORT void obs_transition_set_alignment(obs_source_t *transition, + uint32_t alignment); +EXPORT uint32_t obs_transition_get_alignment(const obs_source_t *transition); + +EXPORT void obs_transition_set_size(obs_source_t *transition, + uint32_t cx, uint32_t cy); +EXPORT void obs_transition_get_size(const obs_source_t *transition, + uint32_t *cx, uint32_t *cy); + +/* function used by transitions */ + +/** + * Enables fixed transitions (videos or specific types of transitions that + * are of fixed duration and linearly interpolated + */ +EXPORT void obs_transition_enable_fixed(obs_source_t *transition, bool enable, + uint32_t duration_ms); +EXPORT bool obs_transition_fixed(obs_source_t *transition); + +typedef void (*obs_transition_video_render_callback_t)(void *data, + gs_texture_t *a, gs_texture_t *b, float t, + uint32_t cx, uint32_t cy); +typedef float (*obs_transition_audio_mix_callback_t)(void *data, float t); + +EXPORT void obs_transition_video_render(obs_source_t *transition, + obs_transition_video_render_callback_t callback); + +EXPORT bool obs_transition_audio_render(obs_source_t *transition, + uint64_t *ts_out, struct obs_source_audio_mix *audio, + uint32_t mixers, size_t channels, size_t sample_rate, + obs_transition_audio_mix_callback_t mix_a_callback, + obs_transition_audio_mix_callback_t mix_b_callback); + +/* swaps transition sources and textures as an optimization and to reduce + * memory usage when switching between transitions */ +EXPORT void obs_transition_swap_begin(obs_source_t *tr_dest, + obs_source_t *tr_source); +EXPORT void obs_transition_swap_end(obs_source_t *tr_dest, + obs_source_t *tr_source); + /* ------------------------------------------------------------------------- */ /* Scenes */