Browse Source

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
Ruwen Hahn 2 years ago
parent
commit
6cdfb0a8b9

+ 97 - 10
libobs/media-io/video-io.c

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

+ 9 - 0
libobs/media-io/video-io.h

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

+ 86 - 4
libobs/obs-encoder.c

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

+ 12 - 0
libobs/obs-internal.h

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

+ 1 - 1
libobs/obs-output.c

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

+ 15 - 2
libobs/obs-video-gpu-encode.c

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

+ 12 - 2
libobs/obs.c

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

+ 16 - 0
libobs/obs.h

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