libobs: Allow configuring frame rate divisor for encoders

This allows encoders/outputs to output at a frame rate that is lower
than the configured base frame rate
This commit is contained in:
Ruwen Hahn 2023-05-25 17:41:30 +02:00 committed by Lain
parent 290570b819
commit 6cdfb0a8b9
8 changed files with 248 additions and 19 deletions

View file

@ -46,6 +46,16 @@ struct video_input {
struct video_frame frame[MAX_CONVERT_BUFFERS];
int cur_frame;
// allow outputting at fractions of main composition FPS,
// e.g. 60 FPS with frame_rate_divisor = 1 turns into 30 FPS
//
// a separate counter is used in favor of using remainder calculations
// to allow "inputs" started at the same time to start on the same frame
// whereas with remainder calculation the frame alignment would depend on
// the total frame count at the time the encoder was started
uint32_t frame_rate_divisor;
uint32_t frame_rate_divisor_counter;
void (*callback)(void *param, struct video_data *frame);
void *param;
};
@ -77,6 +87,8 @@ struct video_output {
size_t last_added;
struct cached_frame_info cache[MAX_CACHE_SIZE];
struct video_output *parent;
volatile bool raw_active;
volatile long gpu_refs;
};
@ -136,6 +148,17 @@ static inline bool video_output_cur_frame(struct video_output *video)
struct video_input *input = video->inputs.array + i;
struct video_data frame = frame_info->frame;
// an explicit counter is used instead of remainder calculation
// to allow multiple encoders started at the same time to start on
// the same frame
uint32_t skip = input->frame_rate_divisor_counter++;
if (input->frame_rate_divisor_counter ==
input->frame_rate_divisor)
input->frame_rate_divisor_counter = 0;
if (skip)
continue;
if (scale_video_output(input, &frame))
input->callback(input->param, &frame);
}
@ -371,13 +394,37 @@ static inline void reset_frames(video_t *video)
os_atomic_set_long(&video->total_frames, 0);
}
static const video_t *get_const_root(const video_t *video)
{
while (video->parent)
video = video->parent;
return video;
}
static video_t *get_root(video_t *video)
{
while (video->parent)
video = video->parent;
return video;
}
bool video_output_connect(
video_t *video, const struct video_scale_info *conversion,
void (*callback)(void *param, struct video_data *frame), void *param)
{
return video_output_connect2(video, conversion, 1, callback, param);
}
bool video_output_connect2(
video_t *video, const struct video_scale_info *conversion,
uint32_t frame_rate_divisor,
void (*callback)(void *param, struct video_data *frame), void *param)
{
bool success = false;
if (!video || !callback)
video = get_root(video);
if (!video || !callback || frame_rate_divisor == 0)
return false;
pthread_mutex_lock(&video->input_mutex);
@ -389,6 +436,8 @@ bool video_output_connect(
input.callback = callback;
input.param = param;
input.frame_rate_divisor = frame_rate_divisor;
if (conversion) {
input.conversion = *conversion;
} else {
@ -446,6 +495,8 @@ void video_output_disconnect(video_t *video,
if (!video || !callback)
return;
video = get_root(video);
pthread_mutex_lock(&video->input_mutex);
size_t idx = video_get_input_idx(video, callback, param);
@ -468,7 +519,7 @@ bool video_output_active(const video_t *video)
{
if (!video)
return false;
return os_atomic_load_bool(&video->raw_active);
return os_atomic_load_bool(&get_const_root(video)->raw_active);
}
const struct video_output_info *video_output_get_info(const video_t *video)
@ -485,6 +536,8 @@ bool video_output_lock_frame(video_t *video, struct video_frame *frame,
if (!video)
return false;
video = get_root(video);
pthread_mutex_lock(&video->data_mutex);
if (video->available_frames == 0) {
@ -518,6 +571,8 @@ void video_output_unlock_frame(video_t *video)
if (!video)
return;
video = get_root(video);
pthread_mutex_lock(&video->data_mutex);
video->available_frames--;
@ -538,6 +593,8 @@ void video_output_stop(video_t *video)
if (!video)
return;
video = get_root(video);
if (!video->stop) {
video->stop = true;
os_sem_post(video->update_semaphore);
@ -550,22 +607,22 @@ bool video_output_stopped(video_t *video)
if (!video)
return true;
return video->stop;
return get_root(video)->stop;
}
enum video_format video_output_get_format(const video_t *video)
{
return video ? video->info.format : VIDEO_FORMAT_NONE;
return video ? get_const_root(video)->info.format : VIDEO_FORMAT_NONE;
}
uint32_t video_output_get_width(const video_t *video)
{
return video ? video->info.width : 0;
return video ? get_const_root(video)->info.width : 0;
}
uint32_t video_output_get_height(const video_t *video)
{
return video ? video->info.height : 0;
return video ? get_const_root(video)->info.height : 0;
}
double video_output_get_frame_rate(const video_t *video)
@ -573,17 +630,21 @@ double video_output_get_frame_rate(const video_t *video)
if (!video)
return 0.0;
video = get_const_root(video);
return (double)video->info.fps_num / (double)video->info.fps_den;
}
uint32_t video_output_get_skipped_frames(const video_t *video)
{
return (uint32_t)os_atomic_load_long(&video->skipped_frames);
return (uint32_t)os_atomic_load_long(
&get_const_root(video)->skipped_frames);
}
uint32_t video_output_get_total_frames(const video_t *video)
{
return (uint32_t)os_atomic_load_long(&video->total_frames);
return (uint32_t)os_atomic_load_long(
&get_const_root(video)->total_frames);
}
/* Note: These four functions below are a very slight bit of a hack. If the
@ -594,6 +655,8 @@ uint32_t video_output_get_total_frames(const video_t *video)
void video_output_inc_texture_encoders(video_t *video)
{
video = get_root(video);
if (os_atomic_inc_long(&video->gpu_refs) == 1 &&
!os_atomic_load_bool(&video->raw_active)) {
reset_frames(video);
@ -602,6 +665,8 @@ void video_output_inc_texture_encoders(video_t *video)
void video_output_dec_texture_encoders(video_t *video)
{
video = get_root(video);
if (os_atomic_dec_long(&video->gpu_refs) == 0 &&
!os_atomic_load_bool(&video->raw_active)) {
log_skipped(video);
@ -610,10 +675,32 @@ void video_output_dec_texture_encoders(video_t *video)
void video_output_inc_texture_frames(video_t *video)
{
os_atomic_inc_long(&video->total_frames);
os_atomic_inc_long(&get_root(video)->total_frames);
}
void video_output_inc_texture_skipped_frames(video_t *video)
{
os_atomic_inc_long(&video->skipped_frames);
os_atomic_inc_long(&get_root(video)->skipped_frames);
}
video_t *video_output_create_with_frame_rate_divisor(video_t *video,
uint32_t divisor)
{
// `divisor == 1` would result in the same frame rate,
// resulting in an unnecessary additional video output
if (!video || divisor == 0 || divisor == 1)
return NULL;
video_t *new_video = bzalloc(sizeof(video_t));
memcpy(new_video, video, sizeof(*new_video));
new_video->parent = video;
new_video->info.fps_den *= divisor;
return new_video;
}
void video_output_free_frame_rate_divisor(video_t *video)
{
if (video && video->parent)
bfree(video);
}

View file

@ -302,6 +302,11 @@ EXPORT bool
video_output_connect(video_t *video, const struct video_scale_info *conversion,
void (*callback)(void *param, struct video_data *frame),
void *param);
EXPORT bool
video_output_connect2(video_t *video, const struct video_scale_info *conversion,
uint32_t frame_rate_divisor,
void (*callback)(void *param, struct video_data *frame),
void *param);
EXPORT void video_output_disconnect(video_t *video,
void (*callback)(void *param,
struct video_data *frame),
@ -331,6 +336,10 @@ extern void video_output_dec_texture_encoders(video_t *video);
extern void video_output_inc_texture_frames(video_t *video);
extern void video_output_inc_texture_skipped_frames(video_t *video);
extern video_t *video_output_create_with_frame_rate_divisor(video_t *video,
uint32_t divisor);
extern void video_output_free_frame_rate_divisor(video_t *video);
#ifdef __cplusplus
}
#endif

View file

@ -112,6 +112,10 @@ create_encoder(const char *id, enum obs_encoder_type type, const char *name,
obs_context_data_insert(&encoder->context, &obs->data.encoders_mutex,
&obs->data.first_encoder);
if (type == OBS_ENCODER_VIDEO) {
encoder->frame_rate_divisor = 1;
}
blog(LOG_DEBUG, "encoder '%s' (%s) created", name, id);
return encoder;
}
@ -309,8 +313,9 @@ static void add_connection(struct obs_encoder *encoder)
if (gpu_encode_available(encoder)) {
start_gpu_encode(encoder);
} else {
start_raw_video(encoder->media, &info, receive_video,
encoder);
start_raw_video(encoder->media, &info,
encoder->frame_rate_divisor,
receive_video, encoder);
}
}
@ -379,6 +384,9 @@ static void obs_encoder_actually_destroy(obs_encoder_t *encoder)
bfree((void *)encoder->info.id);
if (encoder->last_error_message)
bfree(encoder->last_error_message);
if (encoder->fps_override)
video_output_free_frame_rate_divisor(
encoder->fps_override);
bfree(encoder);
}
}
@ -660,6 +668,7 @@ void obs_encoder_shutdown(obs_encoder_t *encoder)
encoder->first_received = false;
encoder->offset_usec = 0;
encoder->start_ts = 0;
encoder->frame_rate_divisor_counter = 1;
maybe_clear_encoder_core_video_mix(encoder);
}
obs_encoder_set_last_error(encoder, NULL);
@ -871,6 +880,52 @@ void obs_encoder_set_gpu_scale_type(obs_encoder_t *encoder,
encoder->gpu_scale_type = gpu_scale_type;
}
bool obs_encoder_set_frame_rate_divisor(obs_encoder_t *encoder,
uint32_t frame_rate_divisor)
{
if (!obs_encoder_valid(encoder, "obs_encoder_set_frame_rate_divisor"))
return false;
if (encoder->info.type != OBS_ENCODER_VIDEO) {
blog(LOG_WARNING,
"obs_encoder_set_frame_rate_divisor: "
"encoder '%s' is not a video encoder",
obs_encoder_get_name(encoder));
return false;
}
if (encoder_active(encoder)) {
blog(LOG_WARNING,
"encoder '%s': Cannot set frame rate divisor "
"while the encoder is active",
obs_encoder_get_name(encoder));
return false;
}
if (frame_rate_divisor == 0) {
blog(LOG_WARNING,
"encoder '%s': Cannot set frame "
"rate divisor to 0",
obs_encoder_get_name(encoder));
return false;
}
encoder->frame_rate_divisor = frame_rate_divisor;
if (encoder->fps_override) {
video_output_free_frame_rate_divisor(encoder->fps_override);
encoder->fps_override = NULL;
}
if (encoder->media) {
encoder->fps_override =
video_output_create_with_frame_rate_divisor(
encoder->media, encoder->frame_rate_divisor);
}
return true;
}
bool obs_encoder_scaling_enabled(const obs_encoder_t *encoder)
{
if (!obs_encoder_valid(encoder, "obs_encoder_scaling_enabled"))
@ -947,6 +1002,22 @@ enum obs_scale_type obs_encoder_get_scale_type(obs_encoder_t *encoder)
return encoder->gpu_scale_type;
}
uint32_t obs_encoder_get_frame_rate_divisor(const obs_encoder_t *encoder)
{
if (!obs_encoder_valid(encoder, "obs_encoder_set_frame_rate_divisor"))
return 0;
if (encoder->info.type != OBS_ENCODER_VIDEO) {
blog(LOG_WARNING,
"obs_encoder_set_frame_rate_divisor: "
"encoder '%s' is not a video encoder",
obs_encoder_get_name(encoder));
return 0;
}
return encoder->frame_rate_divisor;
}
uint32_t obs_encoder_get_sample_rate(const obs_encoder_t *encoder)
{
if (!obs_encoder_valid(encoder, "obs_encoder_get_sample_rate"))
@ -1002,11 +1073,22 @@ void obs_encoder_set_video(obs_encoder_t *encoder, video_t *video)
return;
}
if (encoder->fps_override) {
video_output_free_frame_rate_divisor(encoder->fps_override);
encoder->fps_override = NULL;
}
if (video) {
voi = video_output_get_info(video);
encoder->media = video;
encoder->timebase_num = voi->fps_den;
encoder->timebase_den = voi->fps_num;
if (encoder->frame_rate_divisor) {
encoder->fps_override =
video_output_create_with_frame_rate_divisor(
video, encoder->frame_rate_divisor);
}
} else {
encoder->media = NULL;
encoder->timebase_num = 0;
@ -1056,7 +1138,7 @@ video_t *obs_encoder_video(const obs_encoder_t *encoder)
return NULL;
}
return encoder->media;
return encoder->fps_override ? encoder->fps_override : encoder->media;
}
audio_t *obs_encoder_audio(const obs_encoder_t *encoder)
@ -1218,7 +1300,7 @@ bool do_encode(struct obs_encoder *encoder, struct encoder_frame *frame)
encoder->context.settings);
}
pkt.timebase_num = encoder->timebase_num;
pkt.timebase_num = encoder->timebase_num * encoder->frame_rate_divisor;
pkt.timebase_den = encoder->timebase_den;
pkt.encoder = encoder;

View file

@ -526,6 +526,7 @@ extern struct obs_core_video_mix *get_mix_for_video(video_t *video);
extern void
start_raw_video(video_t *video, const struct video_scale_info *conversion,
uint32_t frame_rate_divisor,
void (*callback)(void *param, struct video_data *frame),
void *param);
extern void stop_raw_video(video_t *video,
@ -1228,6 +1229,17 @@ struct obs_encoder {
uint32_t timebase_num;
uint32_t timebase_den;
// allow outputting at fractions of main composition FPS,
// e.g. 60 FPS with frame_rate_divisor = 1 turns into 30 FPS
//
// a separate counter is used in favor of using remainder calculations
// to allow "inputs" started at the same time to start on the same frame
// whereas with remainder calculation the frame alignment would depend on
// the total frame count at the time the encoder was started
uint32_t frame_rate_divisor;
uint32_t frame_rate_divisor_counter; // only used for GPU encoders
video_t *fps_override;
int64_t cur_pts;
struct circlebuf audio_input_buffer[MAX_AV_PLANES];

View file

@ -2264,7 +2264,7 @@ static void hook_data_capture(struct obs_output *output)
if (has_video)
start_raw_video(output->video,
obs_output_get_video_conversion(output),
default_raw_video_callback, output);
1, default_raw_video_callback, output);
if (has_audio)
start_raw_audio(output);
}

View file

@ -70,11 +70,13 @@ static void *gpu_encode_thread(struct obs_core_video_mix *video)
struct encoder_packet pkt = {0};
bool received = false;
bool success;
uint32_t skip = 0;
obs_encoder_t *encoder = encoders.array[i];
struct obs_encoder *pair = encoder->paired_encoder;
pkt.timebase_num = encoder->timebase_num;
pkt.timebase_num = encoder->timebase_num *
encoder->frame_rate_divisor;
pkt.timebase_den = encoder->timebase_den;
pkt.encoder = encoder;
@ -94,6 +96,16 @@ static void *gpu_encode_thread(struct obs_core_video_mix *video)
encoder->context.settings);
}
// an explicit counter is used instead of remainder calculation
// to allow multiple encoders started at the same time to start on
// the same frame
skip = encoder->frame_rate_divisor_counter++;
if (encoder->frame_rate_divisor_counter ==
encoder->frame_rate_divisor)
encoder->frame_rate_divisor_counter = 0;
if (skip)
continue;
if (!encoder->start_ts)
encoder->start_ts = timestamp;
@ -111,7 +123,8 @@ static void *gpu_encode_thread(struct obs_core_video_mix *video)
lock_key = next_key;
encoder->cur_pts += encoder->timebase_num;
encoder->cur_pts += encoder->timebase_num *
encoder->frame_rate_divisor;
}
/* -------------- */

View file

@ -3132,13 +3132,15 @@ struct obs_core_video_mix *get_mix_for_video(video_t *v)
}
void start_raw_video(video_t *v, const struct video_scale_info *conversion,
uint32_t frame_rate_divisor,
void (*callback)(void *param, struct video_data *frame),
void *param)
{
struct obs_core_video_mix *video = get_mix_for_video(v);
if (video)
os_atomic_inc_long(&video->raw_active);
video_output_connect(v, conversion, callback, param);
video_output_connect2(v, conversion, frame_rate_divisor, callback,
param);
}
void stop_raw_video(video_t *v,
@ -3155,9 +3157,17 @@ void obs_add_raw_video_callback(const struct video_scale_info *conversion,
void (*callback)(void *param,
struct video_data *frame),
void *param)
{
obs_add_raw_video_callback2(conversion, 1, callback, param);
}
void obs_add_raw_video_callback2(
const struct video_scale_info *conversion, uint32_t frame_rate_divisor,
void (*callback)(void *param, struct video_data *frame), void *param)
{
struct obs_core_video_mix *video = obs->video.main_mix;
start_raw_video(video->video, conversion, callback, param);
start_raw_video(video->video, conversion, frame_rate_divisor, callback,
param);
}
void obs_remove_raw_video_callback(void (*callback)(void *param,

View file

@ -849,6 +849,9 @@ EXPORT void obs_remove_main_rendered_callback(void (*rendered)(void *param),
EXPORT void obs_add_raw_video_callback(
const struct video_scale_info *conversion,
void (*callback)(void *param, struct video_data *frame), void *param);
EXPORT void obs_add_raw_video_callback2(
const struct video_scale_info *conversion, uint32_t frame_rate_divisor,
void (*callback)(void *param, struct video_data *frame), void *param);
EXPORT void obs_remove_raw_video_callback(
void (*callback)(void *param, struct video_data *frame), void *param);
@ -2420,6 +2423,16 @@ EXPORT void obs_encoder_set_scaled_size(obs_encoder_t *encoder, uint32_t width,
EXPORT void obs_encoder_set_gpu_scale_type(obs_encoder_t *encoder,
enum obs_scale_type gpu_scale_type);
/**
* Set frame rate divisor for a video encoder. This allows recording at
* a partial frame rate compared to the base frame rate, e.g. 60 FPS with
* divisor = 2 will record at 30 FPS, with divisor = 3 at 20, etc.
*
* Can only be called on stopped encoders, changing this on the fly is not supported
*/
EXPORT bool obs_encoder_set_frame_rate_divisor(obs_encoder_t *encoder,
uint32_t divisor);
/** For video encoders, returns true if pre-encode scaling is enabled */
EXPORT bool obs_encoder_scaling_enabled(const obs_encoder_t *encoder);
@ -2435,6 +2448,9 @@ EXPORT bool obs_encoder_gpu_scaling_enabled(obs_encoder_t *encoder);
/** For video encoders, returns GPU scaling type */
EXPORT enum obs_scale_type obs_encoder_get_scale_type(obs_encoder_t *encoder);
/** For video encoders, returns the frame rate divisor (default is 1) */
EXPORT uint32_t obs_encoder_get_frame_rate_divisor(const obs_encoder_t *encoder);
/** For audio encoders, returns the sample rate of the audio */
EXPORT uint32_t obs_encoder_get_sample_rate(const obs_encoder_t *encoder);