diff --git a/docs/sphinx/reference-outputs.rst b/docs/sphinx/reference-outputs.rst index e7c65b6cd..b0d397d05 100644 --- a/docs/sphinx/reference-outputs.rst +++ b/docs/sphinx/reference-outputs.rst @@ -66,6 +66,14 @@ Output Definition Structure (obs_output_info) When this capability flag is used, specifies that this output supports multiple encoded audio tracks simultaneously. + - **OBS_OUTPUT_CAN_PAUSE** - Output supports pausing. + + When this capability flag is used, the output supports pausing. + When an output is paused, raw or encoded audio/video data will be + halted when paused down to the exact point to the closest video + frame. Audio data will be correctly truncated down to the exact + audio sample according to that video frame timing. + .. member:: const char *(*obs_output_info.get_name)(void *type_data) Get the translated name of the output type. @@ -170,13 +178,9 @@ Output Definition Structure (obs_output_info) :return: The properties of the output -.. member:: void (*obs_output_info.pause)(void *data) +.. member:: void (*obs_output_info.unused1)(void *data) - Pauses the output (if the output supports pausing). - - (Author's note: This is currently unimplemented) - - (Optional) + This callback is no longer used. .. member:: uint64_t (*obs_output_info.get_total_bytes)(void *data) @@ -257,6 +261,14 @@ Output Signals | OBS_OUTPUT_NO_SPACE - Ran out of disk space | OBS_OUTPUT_ENCODE_ERROR - Encoder error +**pause** (ptr output) + + Called when the output has been paused. + +**unpause** (ptr output) + + Called when the output has been unpaused. + **starting** (ptr output) Called when the output is starting. @@ -444,11 +456,18 @@ General Output Functions --------------------- -.. function:: void obs_output_pause(obs_output_t *output) +.. function:: bool obs_output_pause(obs_output_t *output, bool pause) Pause an output (if supported by the output). - (Author's Note: Not yet implemented) + :return: *true* if the output was paused successfuly, *false* + otherwise + +--------------------- + +.. function:: bool obs_output_paused(const obs_output_t *output) + + :return: *true* if the output is paused, *false* otherwise --------------------- @@ -808,6 +827,14 @@ Functions used by outputs | OBS_OUTPUT_UNSUPPORTED - The settings, video/audio format, or codecs are unsupported by this output | OBS_OUTPUT_NO_SPACE - Ran out of disk space +--------------------- + +.. function:: uint64_t obs_output_get_pause_offset(obs_output_t *output) + + Returns the current pause offset of the output. Used with raw + outputs to calculate system timestamps when using calculated + timestamps (see FFmpeg output for an example). + .. --------------------------------------------------------------------------- .. _libobs/obs-output.h: https://github.com/jp9000/obs-studio/blob/master/libobs/obs-output.h diff --git a/libobs/obs-encoder.c b/libobs/obs-encoder.c index 404c76a45..a02371728 100644 --- a/libobs/obs-encoder.c +++ b/libobs/obs-encoder.c @@ -48,6 +48,7 @@ static bool init_encoder(struct obs_encoder *encoder, const char *name, pthread_mutex_init_value(&encoder->init_mutex); pthread_mutex_init_value(&encoder->callbacks_mutex); pthread_mutex_init_value(&encoder->outputs_mutex); + pthread_mutex_init_value(&encoder->pause.mutex); if (pthread_mutexattr_init(&attr) != 0) return false; @@ -62,6 +63,8 @@ static bool init_encoder(struct obs_encoder *encoder, const char *name, return false; if (pthread_mutex_init(&encoder->outputs_mutex, NULL) != 0) return false; + if (pthread_mutex_init(&encoder->pause.mutex, NULL) != 0) + return false; if (encoder->orig_info.get_defaults) encoder->orig_info.get_defaults(encoder->context.settings); @@ -264,6 +267,7 @@ static void obs_encoder_actually_destroy(obs_encoder_t *encoder) pthread_mutex_destroy(&encoder->init_mutex); pthread_mutex_destroy(&encoder->callbacks_mutex); pthread_mutex_destroy(&encoder->outputs_mutex); + pthread_mutex_destroy(&encoder->pause.mutex); obs_context_data_free(&encoder->context); if (encoder->owns_info_id) bfree((void *)encoder->info.id); @@ -529,6 +533,16 @@ get_callback_idx(const struct obs_encoder *encoder, return DARRAY_INVALID; } +void pause_reset(struct pause_data *pause) +{ + pthread_mutex_lock(&pause->mutex); + pause->last_video_ts = 0; + pause->ts_start = 0; + pause->ts_end = 0; + pause->ts_offset = 0; + pthread_mutex_unlock(&pause->mutex); +} + static inline void obs_encoder_start_internal( obs_encoder_t *encoder, void (*new_packet)(void *param, struct encoder_packet *packet), @@ -551,6 +565,9 @@ static inline void obs_encoder_start_internal( pthread_mutex_unlock(&encoder->callbacks_mutex); if (first) { + os_atomic_set_bool(&encoder->paused, false); + pause_reset(&encoder->pause); + encoder->cur_pts = 0; add_connection(encoder); } @@ -906,6 +923,10 @@ void send_off_encoder_packet(obs_encoder_t *encoder, bool success, packet_dts_usec(pkt) - encoder->offset_usec; pkt->sys_dts_usec = pkt->dts_usec; + pthread_mutex_lock(&encoder->pause.mutex); + pkt->sys_dts_usec += encoder->pause.ts_offset / 1000; + pthread_mutex_unlock(&encoder->pause.mutex); + pthread_mutex_lock(&encoder->callbacks_mutex); for (size_t i = encoder->callbacks.num; i > 0; i--) { @@ -946,6 +967,39 @@ bool do_encode(struct obs_encoder *encoder, struct encoder_frame *frame) return success; } +static inline bool video_pause_check_internal(struct pause_data *pause, + uint64_t ts) +{ + pause->last_video_ts = ts; + if (!pause->ts_start) { + return false; + } + + if (ts == pause->ts_start) { + return true; + + } else if (ts == pause->ts_end) { + pause->ts_start = 0; + pause->ts_end = 0; + } else { + + return true; + } + + return false; +} + +bool video_pause_check(struct pause_data *pause, uint64_t timestamp) +{ + bool ignore_frame; + + pthread_mutex_lock(&pause->mutex); + ignore_frame = video_pause_check_internal(pause, timestamp); + pthread_mutex_unlock(&pause->mutex); + + return ignore_frame; +} + static const char *receive_video_name = "receive_video"; static void receive_video(void *param, struct video_data *frame) { @@ -962,6 +1016,9 @@ static void receive_video(void *param, struct video_data *frame) } } + if (video_pause_check(&encoder->pause, frame->timestamp)) + goto wait_for_audio; + memset(&enc_frame, 0, sizeof(struct encoder_frame)); for (size_t i = 0; i < MAX_AV_PLANES; i++) { @@ -1110,20 +1167,90 @@ static bool send_audio_data(struct obs_encoder *encoder) return true; } +static void pause_audio(struct pause_data *pause, struct audio_data *data, + size_t sample_rate) +{ + uint64_t cutoff_frames = pause->ts_start - data->timestamp; + cutoff_frames = ns_to_audio_frames(sample_rate, cutoff_frames); + + data->frames = (uint32_t)cutoff_frames; +} + +static void unpause_audio(struct pause_data *pause, struct audio_data *data, + size_t sample_rate) +{ + uint64_t cutoff_frames = pause->ts_end - data->timestamp; + cutoff_frames = ns_to_audio_frames(sample_rate, cutoff_frames); + + data->timestamp = pause->ts_start; + data->frames = data->frames - (uint32_t)cutoff_frames; + pause->ts_start = 0; + pause->ts_end = 0; +} + +static inline bool audio_pause_check_internal(struct pause_data *pause, + struct audio_data *data, + size_t sample_rate) +{ + uint64_t end_ts; + + if (!pause->ts_start) { + return false; + } + + end_ts = + data->timestamp + audio_frames_to_ns(sample_rate, data->frames); + + if (pause->ts_start >= data->timestamp) { + if (pause->ts_start <= end_ts) { + pause_audio(pause, data, sample_rate); + return !data->frames; + } + + } else { + if (pause->ts_end >= data->timestamp && + pause->ts_end <= end_ts) { + unpause_audio(pause, data, sample_rate); + return !data->frames; + } + + return true; + } + + return false; +} + +bool audio_pause_check(struct pause_data *pause, struct audio_data *data, + size_t sample_rate) +{ + bool ignore_audio; + + pthread_mutex_lock(&pause->mutex); + ignore_audio = audio_pause_check_internal(pause, data, sample_rate); + data->timestamp -= pause->ts_offset; + pthread_mutex_unlock(&pause->mutex); + + return ignore_audio; +} + static const char *receive_audio_name = "receive_audio"; -static void receive_audio(void *param, size_t mix_idx, struct audio_data *data) +static void receive_audio(void *param, size_t mix_idx, struct audio_data *in) { profile_start(receive_audio_name); struct obs_encoder *encoder = param; + struct audio_data audio = *in; if (!encoder->first_received) { - encoder->first_raw_ts = data->timestamp; + encoder->first_raw_ts = audio.timestamp; encoder->first_received = true; clear_audio(encoder); } - if (!buffer_audio(encoder, data)) + if (audio_pause_check(&encoder->pause, &audio, encoder->samplerate)) + goto end; + + if (!buffer_audio(encoder, &audio)) goto end; while (encoder->audio_input_buffer[0].size >= @@ -1331,3 +1458,10 @@ uint32_t obs_encoder_get_caps(const obs_encoder_t *encoder) ? encoder->orig_info.caps : 0; } + +bool obs_encoder_paused(const obs_encoder_t *encoder) +{ + return obs_encoder_valid(encoder, "obs_encoder_paused") + ? os_atomic_load_bool(&encoder->paused) + : false; +} diff --git a/libobs/obs-internal.h b/libobs/obs-internal.h index 4b8b2ae2e..06377c25f 100644 --- a/libobs/obs-internal.h +++ b/libobs/obs-internal.h @@ -840,6 +840,19 @@ struct caption_text { struct caption_text *next; }; +struct pause_data { + pthread_mutex_t mutex; + uint64_t last_video_ts; + uint64_t ts_start; + uint64_t ts_end; + uint64_t ts_offset; +}; + +extern bool video_pause_check(struct pause_data *pause, uint64_t timestamp); +extern bool audio_pause_check(struct pause_data *pause, struct audio_data *data, + size_t sample_rate); +extern void pause_reset(struct pause_data *pause); + struct obs_output { struct obs_context_data context; struct obs_output_info info; @@ -878,6 +891,7 @@ struct obs_output { int total_frames; volatile bool active; + volatile bool paused; video_t *video; audio_t *audio; obs_encoder_t *video_encoder; @@ -885,6 +899,8 @@ struct obs_output { obs_service_t *service; size_t mixer_mask; + struct pause_data pause; + struct circlebuf audio_buffer[MAX_AUDIO_MIXES][MAX_AV_PLANES]; uint64_t audio_start_ts; uint64_t video_start_ts; @@ -988,6 +1004,7 @@ struct obs_encoder { enum video_format preferred_format; volatile bool active; + volatile bool paused; bool initialized; /* indicates ownership of the info.id buffer */ @@ -1023,6 +1040,8 @@ struct obs_encoder { pthread_mutex_t callbacks_mutex; DARRAY(struct encoder_callback) callbacks; + struct pause_data pause; + const char *profile_encoder_encode_name; }; diff --git a/libobs/obs-output.c b/libobs/obs-output.c index 6446bec19..03f11bfe1 100644 --- a/libobs/obs-output.c +++ b/libobs/obs-output.c @@ -74,6 +74,8 @@ const char *obs_output_get_display_name(const char *id) static const char *output_signals[] = { "void start(ptr output)", "void stop(ptr output, int code)", + "void pause(ptr output)", + "void unpause(ptr output)", "void starting(ptr output)", "void stopping(ptr output)", "void activate(ptr output)", @@ -105,6 +107,7 @@ obs_output_t *obs_output_create(const char *id, const char *name, pthread_mutex_init_value(&output->interleaved_mutex); pthread_mutex_init_value(&output->delay_mutex); pthread_mutex_init_value(&output->caption_mutex); + pthread_mutex_init_value(&output->pause.mutex); if (pthread_mutex_init(&output->interleaved_mutex, NULL) != 0) goto fail; @@ -112,6 +115,8 @@ obs_output_t *obs_output_create(const char *id, const char *name, goto fail; if (pthread_mutex_init(&output->caption_mutex, NULL) != 0) goto fail; + if (pthread_mutex_init(&output->pause.mutex, NULL) != 0) + goto fail; if (os_event_init(&output->stopping_event, OS_EVENT_TYPE_MANUAL) != 0) goto fail; if (!init_output_handlers(output, name, settings, hotkey_data)) @@ -214,6 +219,7 @@ void obs_output_destroy(obs_output_t *output) clear_audio_buffers(output); os_event_destroy(output->stopping_event); + pthread_mutex_destroy(&output->pause.mutex); pthread_mutex_destroy(&output->caption_mutex); pthread_mutex_destroy(&output->interleaved_mutex); pthread_mutex_destroy(&output->delay_mutex); @@ -353,6 +359,9 @@ void obs_output_actual_stop(obs_output_t *output, bool force, uint64_t ts) if (stopping(output) && !force) return; + + obs_output_pause(output, false); + os_event_reset(output->stopping_event); was_reconnecting = reconnecting(output) && !delay_active(output); @@ -517,17 +526,148 @@ obs_data_t *obs_output_get_settings(const obs_output_t *output) bool obs_output_can_pause(const obs_output_t *output) { return obs_output_valid(output, "obs_output_can_pause") - ? (output->info.pause != NULL) + ? !!(output->info.flags & OBS_OUTPUT_CAN_PAUSE) : false; } -void obs_output_pause(obs_output_t *output) +static inline void end_pause(struct pause_data *pause, uint64_t ts) { - if (!obs_output_valid(output, "obs_output_pause")) - return; + if (!pause->ts_end) { + pause->ts_end = ts; + pause->ts_offset += pause->ts_end - pause->ts_start; + } +} - if (output->info.pause) - output->info.pause(output->context.data); +static inline uint64_t get_closest_v_ts(struct pause_data *pause) +{ + uint64_t interval = obs->video.video_frame_interval_ns; + uint64_t ts = os_gettime_ns(); + + return pause->last_video_ts + + ((ts - pause->last_video_ts + interval) / interval) * interval; +} + +static bool obs_encoded_output_pause(obs_output_t *output, bool pause) +{ + obs_encoder_t *venc; + obs_encoder_t *aenc[MAX_AUDIO_MIXES]; + uint64_t closest_v_ts; + + venc = output->video_encoder; + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) + aenc[i] = output->audio_encoders[i]; + + pthread_mutex_lock(&venc->pause.mutex); + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + if (aenc[i]) { + pthread_mutex_lock(&aenc[i]->pause.mutex); + } + } + + /* ---------------------------- */ + + closest_v_ts = get_closest_v_ts(&venc->pause); + + if (pause) { + os_atomic_set_bool(&venc->paused, true); + venc->pause.ts_start = closest_v_ts; + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + if (aenc[i]) { + os_atomic_set_bool(&aenc[i]->paused, true); + aenc[i]->pause.ts_start = closest_v_ts; + } + } + } else { + os_atomic_set_bool(&venc->paused, false); + end_pause(&venc->pause, closest_v_ts); + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + if (aenc[i]) { + os_atomic_set_bool(&aenc[i]->paused, false); + end_pause(&aenc[i]->pause, closest_v_ts); + } + } + } + + /* ---------------------------- */ + + for (size_t i = MAX_AUDIO_MIXES; i > 0; i--) { + if (aenc[i - 1]) { + pthread_mutex_unlock(&aenc[i - 1]->pause.mutex); + } + } + pthread_mutex_unlock(&venc->pause.mutex); + + return true; +} + +static bool obs_raw_output_pause(obs_output_t *output, bool pause) +{ + bool success; + uint64_t closest_v_ts; + + pthread_mutex_lock(&output->pause.mutex); + closest_v_ts = get_closest_v_ts(&output->pause); + if (pause) { + success = !output->pause.ts_start; + if (success) + output->pause.ts_start = closest_v_ts; + } else { + success = !!output->pause.ts_start; + if (success) + end_pause(&output->pause, closest_v_ts); + } + pthread_mutex_unlock(&output->pause.mutex); + + return success; +} + +bool obs_output_pause(obs_output_t *output, bool pause) +{ + bool success; + + if (!obs_output_valid(output, "obs_output_pause")) + return false; + if ((output->info.flags & OBS_OUTPUT_CAN_PAUSE) == 0) + return false; + if (!os_atomic_load_bool(&output->active)) + return false; + if (os_atomic_load_bool(&output->paused) == pause) + return true; + + success = ((output->info.flags & OBS_OUTPUT_ENCODED) != 0) + ? obs_encoded_output_pause(output, pause) + : obs_raw_output_pause(output, pause); + if (success) { + os_atomic_set_bool(&output->paused, pause); + do_output_signal(output, pause ? "pause" : "unpause"); + + blog(LOG_INFO, "output %s %spaused", output->context.name, + pause ? "" : "un"); + } + return success; +} + +bool obs_output_paused(const obs_output_t *output) +{ + return obs_output_valid(output, "obs_output_paused") + ? os_atomic_load_bool(&output->paused) + : false; +} + +uint64_t obs_output_get_pause_offset(obs_output_t *output) +{ + uint64_t offset; + + if (!obs_output_valid(output, "obs_output_get_pause_offset")) + return 0; + + pthread_mutex_lock(&output->pause.mutex); + offset = output->pause.ts_offset; + pthread_mutex_unlock(&output->pause.mutex); + + return offset; } signal_handler_t *obs_output_get_signal_handler(const obs_output_t *output) @@ -1532,18 +1672,29 @@ static void default_encoded_callback(void *param, struct encoder_packet *packet) static void default_raw_video_callback(void *param, struct video_data *frame) { struct obs_output *output = param; + + if (video_pause_check(&output->pause, frame->timestamp)) + return; + if (data_active(output)) output->info.raw_video(output->context.data, frame); output->total_frames++; - - if (!output->video_start_ts) { - output->video_start_ts = frame->timestamp; - } } static bool prepare_audio(struct obs_output *output, const struct audio_data *old, struct audio_data *new) { + if (!output->video_start_ts) { + pthread_mutex_lock(&output->pause.mutex); + output->video_start_ts = output->pause.last_video_ts; + pthread_mutex_unlock(&output->pause.mutex); + } + + if (!output->video_start_ts) + return false; + + /* ------------------ */ + *new = *old; if (old->timestamp < output->video_start_ts) { @@ -1580,10 +1731,10 @@ static void default_raw_audio_callback(void *param, size_t mix_idx, /* -------------- */ - if (!output->video_start_ts) - return; if (!prepare_audio(output, in, &out)) return; + if (audio_pause_check(&output->pause, &out, output->sample_rate)) + return; if (!output->audio_start_ts) { output->audio_start_ts = out.timestamp; } @@ -1610,6 +1761,10 @@ static void default_raw_audio_callback(void *param, size_t mix_idx, audio_frames_to_ns(output->sample_rate, output->total_audio_frames); + pthread_mutex_lock(&output->pause.mutex); + out.timestamp += output->pause.ts_offset; + pthread_mutex_unlock(&output->pause.mutex); + output->total_audio_frames += AUDIO_OUTPUT_FRAMES; if (output->info.raw_audio2) @@ -1906,6 +2061,8 @@ static void reset_raw_output(obs_output_t *output) output->planes = get_audio_planes(info.format, info.speakers); output->total_audio_frames = 0; output->audio_size = get_audio_size(info.format, info.speakers, 1); + + pause_reset(&output->pause); } bool obs_output_begin_data_capture(obs_output_t *output, uint32_t flags) diff --git a/libobs/obs-output.h b/libobs/obs-output.h index b1a5f44c4..51093f490 100644 --- a/libobs/obs-output.h +++ b/libobs/obs-output.h @@ -27,6 +27,7 @@ extern "C" { #define OBS_OUTPUT_ENCODED (1 << 2) #define OBS_OUTPUT_SERVICE (1 << 3) #define OBS_OUTPUT_MULTI_TRACK (1 << 4) +#define OBS_OUTPUT_CAN_PAUSE (1 << 5) struct encoder_packet; @@ -56,7 +57,7 @@ struct obs_output_info { obs_properties_t *(*get_properties)(void *data); - void (*pause)(void *data); + void (*unused1)(void *data); uint64_t (*get_total_bytes)(void *data); diff --git a/libobs/obs-video-gpu-encode.c b/libobs/obs-video-gpu-encode.c index 221501b30..808b55c3c 100644 --- a/libobs/obs-video-gpu-encode.c +++ b/libobs/obs-video-gpu-encode.c @@ -86,6 +86,9 @@ static void *gpu_encode_thread(void *unused) } } + if (video_pause_check(&encoder->pause, timestamp)) + continue; + if (!encoder->start_ts) encoder->start_ts = timestamp; diff --git a/libobs/obs.h b/libobs/obs.h index 90e464fce..58e862a0d 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -1692,7 +1692,10 @@ EXPORT void obs_output_update(obs_output_t *output, obs_data_t *settings); EXPORT bool obs_output_can_pause(const obs_output_t *output); /** Pauses the output (if the functionality is allowed by the output */ -EXPORT void obs_output_pause(obs_output_t *output); +EXPORT bool obs_output_pause(obs_output_t *output, bool pause); + +/** Returns whether output is paused */ +EXPORT bool obs_output_paused(const obs_output_t *output); /* Gets the current output settings string */ EXPORT obs_data_t *obs_output_get_settings(const obs_output_t *output); @@ -1867,6 +1870,8 @@ EXPORT void obs_output_end_data_capture(obs_output_t *output); */ EXPORT void obs_output_signal_stop(obs_output_t *output, int code); +EXPORT uint64_t obs_output_get_pause_offset(obs_output_t *output); + /* ------------------------------------------------------------------------- */ /* Encoders */ @@ -2031,6 +2036,9 @@ EXPORT void obs_encoder_packet_release(struct encoder_packet *packet); EXPORT void *obs_encoder_create_rerouted(obs_encoder_t *encoder, const char *reroute_id); +/** Returns whether encoder is paused */ +EXPORT bool obs_encoder_paused(const obs_encoder_t *output); + /* ------------------------------------------------------------------------- */ /* Stream Services */