瀏覽代碼

obs-ffmpeg: Split ffmpeg_muxer output file by size or time

This commit adds 3 new properties to split output file in the output
`ffmpeg_muxer`.
- `max_time_sec` specifies the limit in seconds.
- `max_size_mb` specifies the limit in megabytes.
- `allow_overwrite` specifies to test an existing file.
If both `max_time_sec` and `max_size_mb` are not positive, the split
file feature won't be enabled.
Another output ffmpeg_mpegts_muxer shares the code but is not affected
since the output is used only for streaming.
Norihiro Kamae 4 年之前
父節點
當前提交
ce92f441b5

+ 37 - 0
plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.c

@@ -821,6 +821,35 @@ static inline bool ffmpeg_mux_packet(struct ffmpeg_mux *ffm, uint8_t *buf,
 	return ret >= 0;
 }
 
+static inline bool read_change_file(struct ffmpeg_mux *ffm, uint32_t size,
+				    struct resize_buf *filename, int argc,
+				    char **argv)
+{
+	resize_buf_resize(filename, size + 1);
+	if (safe_read(filename->buf, size) != size) {
+		return false;
+	}
+	filename->buf[size] = 0;
+
+	fprintf(stderr, "info: New output file name: %s\n", filename->buf);
+
+	int ret;
+	char *argv1_backup = argv[1];
+	argv[1] = (char *)filename->buf;
+
+	ffmpeg_mux_free(ffm);
+
+	ret = ffmpeg_mux_init(ffm, argc, argv);
+	if (ret != FFM_SUCCESS) {
+		fprintf(stderr, "Couldn't initialize muxer\n");
+		return false;
+	}
+
+	argv[1] = argv1_backup;
+
+	return true;
+}
+
 /* ------------------------------------------------------------------------- */
 
 #ifdef _WIN32
@@ -832,6 +861,7 @@ int main(int argc, char *argv[])
 	struct ffm_packet_info info = {0};
 	struct ffmpeg_mux ffm = {0};
 	struct resize_buf rb = {0};
+	struct resize_buf rb_filename = {0};
 	bool fail = false;
 	int ret;
 
@@ -864,6 +894,12 @@ int main(int argc, char *argv[])
 	}
 
 	while (!fail && safe_read(&info, sizeof(info)) == sizeof(info)) {
+		if (info.type == FFM_PACKET_CHANGE_FILE) {
+			fail = !read_change_file(&ffm, info.size, &rb_filename,
+						 argc, argv);
+			continue;
+		}
+
 		resize_buf_resize(&rb, info.size);
 
 		if (safe_read(rb.buf, info.size) == info.size) {
@@ -875,6 +911,7 @@ int main(int argc, char *argv[])
 
 	ffmpeg_mux_free(&ffm);
 	resize_buf_free(&rb);
+	resize_buf_free(&rb_filename);
 
 #ifdef _WIN32
 	for (int i = 0; i < argc; i++)

+ 1 - 0
plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.h

@@ -22,6 +22,7 @@
 enum ffm_packet_type {
 	FFM_PACKET_VIDEO,
 	FFM_PACKET_AUDIO,
+	FFM_PACKET_CHANGE_FILE,
 };
 
 #define FFM_SUCCESS 0

+ 136 - 3
plugins/obs-ffmpeg/obs-ffmpeg-mux.c

@@ -87,6 +87,9 @@ static void *ffmpeg_mux_create(obs_data_t *settings, obs_output_t *output)
 	if (obs_output_get_flags(output) & OBS_OUTPUT_SERVICE)
 		stream->is_network = true;
 
+	signal_handler_t *sh = obs_output_get_signal_handler(output);
+	signal_handler_add(sh, "void file_changed(string next_file)");
+
 	UNUSED_PARAMETER(settings);
 	return stream;
 }
@@ -324,8 +327,20 @@ static bool ffmpeg_mux_start(void *data)
 		if (!service)
 			return false;
 		path = obs_service_get_url(service);
+		stream->split_file = false;
 	} else {
 		path = obs_data_get_string(settings, "path");
+
+		stream->max_time =
+			obs_data_get_int(settings, "max_time_sec") * 1000000LL;
+		stream->max_size = obs_data_get_int(settings, "max_size_mb") *
+				   (1024 * 1024);
+		stream->split_file = stream->max_time > 0 ||
+				     stream->max_size > 0;
+		stream->allow_overwrite =
+			obs_data_get_bool(settings, "allow_overwrite");
+		stream->cur_size = 0;
+		stream->sent_headers = false;
 	}
 
 	if (!stream->is_network) {
@@ -459,14 +474,41 @@ static void signal_failure(struct ffmpeg_muxer *stream)
 	os_atomic_set_bool(&stream->capturing, false);
 }
 
-static void generate_filename(struct ffmpeg_muxer *stream, struct dstr *dst)
+static void find_best_filename(struct dstr *path, bool space)
+{
+	int num = 2;
+
+	if (!os_file_exists(path->array))
+		return;
+
+	const char *ext = strrchr(path->array, '.');
+	if (!ext)
+		return;
+
+	size_t extstart = ext - path->array;
+	struct dstr testpath;
+	dstr_init_copy_dstr(&testpath, path);
+	for (;;) {
+		dstr_resize(&testpath, extstart);
+		dstr_catf(&testpath, space ? " (%d)" : "_%d", num++);
+		dstr_cat(&testpath, ext);
+
+		if (!os_file_exists(testpath.array)) {
+			dstr_free(path);
+			dstr_init_move(path, &testpath);
+			break;
+		}
+	}
+}
+
+static void generate_filename(struct ffmpeg_muxer *stream, struct dstr *dst,
+			      bool overwrite)
 {
 	obs_data_t *settings = obs_output_get_settings(stream->output);
 	const char *dir = obs_data_get_string(settings, "directory");
 	const char *fmt = obs_data_get_string(settings, "format");
 	const char *ext = obs_data_get_string(settings, "extension");
 	bool space = obs_data_get_bool(settings, "allow_spaces");
-	// TODO: allow_overwrite
 
 	char *filename = os_generate_formatted_filename(ext, space, fmt);
 
@@ -483,6 +525,9 @@ static void generate_filename(struct ffmpeg_muxer *stream, struct dstr *dst)
 		*slash = '/';
 	}
 
+	if (!overwrite)
+		find_best_filename(dst, space);
+
 	bfree(filename);
 	obs_data_release(settings);
 }
@@ -516,6 +561,10 @@ bool write_packet(struct ffmpeg_muxer *stream, struct encoder_packet *packet)
 	}
 
 	stream->total_bytes += packet->size;
+
+	if (stream->split_file)
+		stream->cur_size += packet->size;
+
 	return true;
 }
 
@@ -561,6 +610,82 @@ bool send_headers(struct ffmpeg_muxer *stream)
 	return true;
 }
 
+static inline bool should_split(struct ffmpeg_muxer *stream,
+				struct encoder_packet *packet)
+{
+	/* split at video frame */
+	if (packet->type != OBS_ENCODER_VIDEO)
+		return false;
+
+	/* don't split group of pictures */
+	if (!packet->keyframe)
+		return false;
+
+	/* reached maximum file size */
+	if (stream->max_size > 0 &&
+	    stream->cur_size + (int64_t)packet->size >= stream->max_size)
+		return true;
+
+	/* reached maximum duration */
+	if (stream->max_time > 0 &&
+	    packet->dts_usec - stream->cur_time >= stream->max_time)
+		return true;
+
+	return false;
+}
+
+static bool send_new_filename(struct ffmpeg_muxer *stream, const char *filename)
+{
+	size_t ret;
+	uint32_t size = strlen(filename);
+	struct ffm_packet_info info = {.type = FFM_PACKET_CHANGE_FILE,
+				       .size = size};
+
+	ret = os_process_pipe_write(stream->pipe, (const uint8_t *)&info,
+				    sizeof(info));
+	if (ret != sizeof(info)) {
+		warn("os_process_pipe_write for info structure failed");
+		signal_failure(stream);
+		return false;
+	}
+
+	ret = os_process_pipe_write(stream->pipe, (const uint8_t *)filename,
+				    size);
+	if (ret != size) {
+		warn("os_process_pipe_write for packet data failed");
+		signal_failure(stream);
+		return false;
+	}
+
+	return true;
+}
+
+static bool prepare_split_file(struct ffmpeg_muxer *stream,
+			       struct encoder_packet *packet)
+{
+	generate_filename(stream, &stream->path, stream->allow_overwrite);
+	info("Changing output file to '%s'", stream->path.array);
+
+	if (!send_new_filename(stream, stream->path.array)) {
+		warn("Failed to send new file name");
+		return false;
+	}
+
+	calldata_t cd = {0};
+	signal_handler_t *sh = obs_output_get_signal_handler(stream->output);
+	calldata_set_string(&cd, "next_file", stream->path.array);
+	signal_handler_signal(sh, "file_changed", &cd);
+	calldata_free(&cd);
+
+	if (!send_headers(stream))
+		return false;
+
+	stream->cur_size = 0;
+	stream->cur_time = packet->dts_usec;
+
+	return true;
+}
+
 static void ffmpeg_mux_data(void *data, struct encoder_packet *packet)
 {
 	struct ffmpeg_muxer *stream = data;
@@ -574,11 +699,19 @@ static void ffmpeg_mux_data(void *data, struct encoder_packet *packet)
 		return;
 	}
 
+	if (stream->split_file && should_split(stream, packet)) {
+		if (!prepare_split_file(stream, packet))
+			return;
+	}
+
 	if (!stream->sent_headers) {
 		if (!send_headers(stream))
 			return;
 
 		stream->sent_headers = true;
+
+		if (stream->split_file)
+			stream->cur_time = packet->dts_usec;
 	}
 
 	if (stopping(stream)) {
@@ -937,7 +1070,7 @@ static void replay_buffer_save(struct ffmpeg_muxer *stream)
 			      audio_dts_offsets);
 	}
 
-	generate_filename(stream, &stream->path);
+	generate_filename(stream, &stream->path, true);
 
 	os_atomic_set_bool(&stream->muxing, true);
 	stream->mux_thread_joinable = pthread_create(&stream->mux_thread, NULL,

+ 5 - 1
plugins/obs-ffmpeg/obs-ffmpeg-mux.h

@@ -24,11 +24,13 @@ struct ffmpeg_muxer {
 	struct dstr muxer_settings;
 	struct dstr stream_key;
 
-	/* replay buffer */
+	/* replay buffer and split file */
 	int64_t cur_size;
 	int64_t cur_time;
 	int64_t max_size;
 	int64_t max_time;
+
+	/* replay buffer */
 	int64_t save_ts;
 	int keyframes;
 	obs_hotkey_id hotkey;
@@ -51,6 +53,8 @@ struct ffmpeg_muxer {
 	int64_t last_dts_usec;
 
 	bool is_network;
+	bool split_file;
+	bool allow_overwrite;
 };
 
 bool stopping(struct ffmpeg_muxer *stream);