From ce92f441b5cc33b3f5c7ee4d1b6d087a27d47e17 Mon Sep 17 00:00:00 2001 From: Norihiro Kamae Date: Fri, 1 Oct 2021 23:44:21 +0900 Subject: [PATCH] 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. --- plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.c | 37 ++++++ plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.h | 1 + plugins/obs-ffmpeg/obs-ffmpeg-mux.c | 139 ++++++++++++++++++++- plugins/obs-ffmpeg/obs-ffmpeg-mux.h | 6 +- 4 files changed, 179 insertions(+), 4 deletions(-) diff --git a/plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.c b/plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.c index c07e22abe..7755d9c29 100644 --- a/plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.c +++ b/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++) diff --git a/plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.h b/plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.h index ac3bc7729..b6b472fb7 100644 --- a/plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.h +++ b/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 diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-mux.c b/plugins/obs-ffmpeg/obs-ffmpeg-mux.c index 8bd8a807d..18eae54d7 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-mux.c +++ b/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, diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-mux.h b/plugins/obs-ffmpeg/obs-ffmpeg-mux.h index eb9dbe1df..d0de39272 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-mux.h +++ b/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);