libobs: Implement pausing of outputs

This implements pausing of outputs.  To accomplish this, raw audio/video
data is halted to the encoders or raw output.  Pausing is as precisely
timed as possible according to the timing of the obs_output_pause call,
and audio data will be spliced down to the exact audio sample in
accordance to that timing at the start/end marks.

Outputs that support this (outputs used for recording) can set the
OBS_OUTPUT_CAN_PAUSE capability flag.
master
jp9000 2019-07-07 12:27:13 -07:00
parent 3b581a3727
commit 153fa6337f
7 changed files with 374 additions and 25 deletions

View File

@ -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

View File

@ -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;
}

View File

@ -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;
};

View File

@ -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)

View File

@ -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);

View File

@ -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;

View File

@ -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 */