From 55534d5d8d4cf67a23cc5c0b9add08a62759a7ad Mon Sep 17 00:00:00 2001 From: jp9000 Date: Sat, 21 Mar 2015 18:22:03 -0700 Subject: [PATCH] obs-filters: Add async delay filter Adds a filter for delaying asynchronous video/audio data, for example from webcams, video devices, or media playback sources. Mainly intended to allow users to sync up their webcams to other devices. --- plugins/CMakeLists.txt | 1 + plugins/obs-filters/CMakeLists.txt | 12 ++ plugins/obs-filters/async-delay-filter.c | 243 ++++++++++++++++++++++ plugins/obs-filters/data/locale/en-US.ini | 2 + plugins/obs-filters/obs-filters.c | 13 ++ 5 files changed, 271 insertions(+) create mode 100644 plugins/obs-filters/CMakeLists.txt create mode 100644 plugins/obs-filters/async-delay-filter.c create mode 100644 plugins/obs-filters/data/locale/en-US.ini create mode 100644 plugins/obs-filters/obs-filters.c diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index dbeead7a..34957543 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -22,5 +22,6 @@ add_subdirectory(obs-x264) add_subdirectory(obs-libfdk) add_subdirectory(obs-ffmpeg) add_subdirectory(obs-outputs) +add_subdirectory(obs-filters) add_subdirectory(rtmp-services) add_subdirectory(text-freetype2) diff --git a/plugins/obs-filters/CMakeLists.txt b/plugins/obs-filters/CMakeLists.txt new file mode 100644 index 00000000..3313a020 --- /dev/null +++ b/plugins/obs-filters/CMakeLists.txt @@ -0,0 +1,12 @@ +project(obs-filters) + +set(obs-filters_SOURCES + obs-filters.c + async-delay-filter.c) + +add_library(obs-filters MODULE + ${obs-filters_SOURCES}) +target_link_libraries(obs-filters + libobs) + +install_obs_plugin_with_data(obs-filters data) diff --git a/plugins/obs-filters/async-delay-filter.c b/plugins/obs-filters/async-delay-filter.c new file mode 100644 index 00000000..b9662e3f --- /dev/null +++ b/plugins/obs-filters/async-delay-filter.c @@ -0,0 +1,243 @@ +#include +#include + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000ULL +#endif + +#ifndef MSEC_TO_NSEC +#define MSEC_TO_NSEC 1000000ULL +#endif + +#define SETTING_DELAY_MS "delay_ms" + +#define TEXT_DELAY_MS obs_module_text("DelayMs") + +struct async_delay_data { + obs_source_t *context; + + /* contains struct obs_source_frame* */ + struct circlebuf video_frames; + + /* stores the audio data */ + struct circlebuf audio_frames; + struct obs_audio_data audio_output; + + uint64_t last_video_ts; + uint64_t last_audio_ts; + uint64_t interval; + uint64_t samplerate; + bool video_delay_reached; + bool audio_delay_reached; + bool reset_video; + bool reset_audio; +}; + +static const char *async_delay_filter_name(void) +{ + return obs_module_text("AsyncDelayFilter"); +} + +static void free_video_data(struct async_delay_data *filter, + obs_source_t *parent) +{ + while (filter->video_frames.size) { + struct obs_source_frame *frame; + + circlebuf_pop_front(&filter->video_frames, &frame, + sizeof(struct obs_source_frame*)); + obs_source_release_frame(parent, frame); + } +} + +static inline void free_audio_packet(struct obs_audio_data *audio) +{ + for (size_t i = 0; i < MAX_AV_PLANES; i++) + bfree(audio->data[i]); + memset(audio, 0, sizeof(*audio)); +} + +static void free_audio_data(struct async_delay_data *filter) +{ + while (filter->audio_frames.size) { + struct obs_audio_data audio; + + circlebuf_pop_front(&filter->audio_frames, &audio, + sizeof(struct obs_audio_data)); + free_audio_packet(&audio); + } +} + +static void async_delay_filter_update(void *data, obs_data_t *settings) +{ + struct async_delay_data *filter = data; + uint64_t new_interval = (uint64_t)obs_data_get_int(settings, + SETTING_DELAY_MS) * MSEC_TO_NSEC; + + if (new_interval < filter->interval) + free_video_data(filter, obs_filter_get_parent(filter->context)); + + filter->reset_audio = true; + filter->reset_video = true; + filter->interval = new_interval; + filter->video_delay_reached = false; + filter->audio_delay_reached = false; +} + +static void *async_delay_filter_create(obs_data_t *settings, + obs_source_t *context) +{ + struct async_delay_data *filter = bzalloc(sizeof(*filter)); + struct obs_audio_info oai; + + filter->context = context; + async_delay_filter_update(filter, settings); + + obs_get_audio_info(&oai); + filter->samplerate = oai.samples_per_sec; + + return filter; +} + +static void async_delay_filter_destroy(void *data) +{ + struct async_delay_data *filter = data; + + free_audio_packet(&filter->audio_output); + circlebuf_free(&filter->video_frames); + circlebuf_free(&filter->audio_frames); + bfree(data); +} + +static obs_properties_t *async_delay_filter_properties(void *data) +{ + obs_properties_t *props = obs_properties_create(); + + obs_properties_add_int(props, SETTING_DELAY_MS, TEXT_DELAY_MS, + 0, 6000, 1); + + UNUSED_PARAMETER(data); + return props; +} + +static void async_delay_filter_remove(void *data, obs_source_t *parent) +{ + struct async_delay_data *filter = data; + + free_video_data(filter, parent); + free_audio_data(filter); +} + +/* due to the fact that we need timing information to be consistent in order to + * measure the current interval of data, if there is an unexpected hiccup or + * jump with the timestamps, reset the cached delay data and start again to + * ensure that the timing is consistent */ +static inline bool is_timestamp_jump(uint64_t ts, uint64_t prev_ts) +{ + return ts < prev_ts || (ts - prev_ts) > SEC_TO_NSEC; +} + +static struct obs_source_frame *async_delay_filter_video(void *data, + struct obs_source_frame *frame) +{ + struct async_delay_data *filter = data; + obs_source_t *parent = obs_filter_get_parent(filter->context); + struct obs_source_frame *output; + uint64_t cur_interval; + + if (filter->reset_video || + is_timestamp_jump(frame->timestamp, filter->last_video_ts)) { + free_video_data(filter, parent); + filter->video_delay_reached = false; + filter->reset_video = false; + } + + filter->last_video_ts = frame->timestamp; + + circlebuf_push_back(&filter->video_frames, &frame, + sizeof(struct obs_source_frame*)); + circlebuf_peek_front(&filter->video_frames, &output, + sizeof(struct obs_source_frame*)); + + cur_interval = frame->timestamp - output->timestamp; + if (!filter->video_delay_reached && cur_interval < filter->interval) + return NULL; + + circlebuf_pop_front(&filter->video_frames, NULL, + sizeof(struct obs_source_frame*)); + + if (!filter->video_delay_reached) + filter->video_delay_reached = true; + + return output; +} + +/* NOTE: Delaying audio shouldn't be necessary because the audio subsystem will + * automatically sync audio to video frames */ + +/* #define DELAY_AUDIO */ + +#ifdef DELAY_AUDIO +static struct obs_audio_data *async_delay_filter_audio(void *data, + struct obs_audio_data *audio) +{ + struct async_delay_data *filter = data; + struct obs_audio_data cached = *audio; + uint64_t cur_interval; + uint64_t duration; + uint64_t end_ts; + + if (filter->reset_audio || + is_timestamp_jump(audio->timestamp, filter->last_audio_ts)) { + free_audio_data(filter); + filter->audio_delay_reached = false; + filter->reset_audio = false; + } + + filter->last_audio_ts = audio->timestamp; + + duration = (uint64_t)audio->frames * SEC_TO_NSEC / filter->samplerate; + end_ts = audio->timestamp + duration; + + for (size_t i = 0; i < MAX_AV_PLANES; i++) { + if (!audio->data[i]) + break; + + cached.data[i] = bmemdup(audio->data[i], + audio->frames * sizeof(float)); + } + + free_audio_packet(&filter->audio_output); + + circlebuf_push_back(&filter->audio_frames, &cached, sizeof(cached)); + circlebuf_peek_front(&filter->audio_frames, &cached, sizeof(cached)); + + cur_interval = end_ts - cached.timestamp; + if (!filter->audio_delay_reached && cur_interval < filter->interval) + return NULL; + + circlebuf_pop_front(&filter->audio_frames, NULL, sizeof(cached)); + memcpy(&filter->audio_output, &cached, sizeof(cached)); + + if (!filter->audio_delay_reached) + filter->audio_delay_reached = true; + + return &filter->audio_output; +} +#endif + +struct obs_source_info async_delay_filter = { + .id = "async_delay_filter", + .type = OBS_SOURCE_TYPE_FILTER, + .output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_ASYNC, + .get_name = async_delay_filter_name, + .create = async_delay_filter_create, + .destroy = async_delay_filter_destroy, + .update = async_delay_filter_update, + .get_properties = async_delay_filter_properties, + .filter_video = async_delay_filter_video, +#ifdef DELAY_AUDIO + .filter_audio = async_delay_filter_audio, +#endif + .filter_remove = async_delay_filter_remove +}; diff --git a/plugins/obs-filters/data/locale/en-US.ini b/plugins/obs-filters/data/locale/en-US.ini new file mode 100644 index 00000000..c8905c7e --- /dev/null +++ b/plugins/obs-filters/data/locale/en-US.ini @@ -0,0 +1,2 @@ +AsyncDelayFilter="Video Delay (Async)" +DelayMs="Delay (milliseconds)" diff --git a/plugins/obs-filters/obs-filters.c b/plugins/obs-filters/obs-filters.c new file mode 100644 index 00000000..2f7c5383 --- /dev/null +++ b/plugins/obs-filters/obs-filters.c @@ -0,0 +1,13 @@ +#include + +OBS_DECLARE_MODULE() + +OBS_MODULE_USE_DEFAULT_LOCALE("obs-filters", "en-US") + +extern struct obs_source_info async_delay_filter; + +bool obs_module_load(void) +{ + obs_register_source(&async_delay_filter); + return true; +} -- GitLab