From d64adb67840b274d2661d6270b0d7d050d9ab3a9 Mon Sep 17 00:00:00 2001 From: Norihiro Kamae Date: Fri, 1 Oct 2021 14:16:54 +0900 Subject: [PATCH 1/5] obs-ffmpeg: separate generate_filename function --- plugins/obs-ffmpeg/obs-ffmpeg-mux.c | 57 +++++++++++++++-------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-mux.c b/plugins/obs-ffmpeg/obs-ffmpeg-mux.c index d82e2cd94..8bd8a807d 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-mux.c +++ b/plugins/obs-ffmpeg/obs-ffmpeg-mux.c @@ -459,6 +459,34 @@ 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) +{ + 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); + + dstr_copy(dst, dir); + dstr_replace(dst, "\\", "/"); + if (dstr_end(dst) != '/') + dstr_cat_ch(dst, '/'); + dstr_cat(dst, filename); + + char *slash = strrchr(dst->array, '/'); + if (slash) { + *slash = 0; + os_mkdirs(dst->array); + *slash = '/'; + } + + bfree(filename); + obs_data_release(settings); +} + bool write_packet(struct ffmpeg_muxer *stream, struct encoder_packet *packet) { bool is_video = packet->type == OBS_ENCODER_VIDEO; @@ -909,34 +937,7 @@ static void replay_buffer_save(struct ffmpeg_muxer *stream) audio_dts_offsets); } - /* ---------------------------- */ - /* generate filename */ - - 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"); - - char *filename = os_generate_formatted_filename(ext, space, fmt); - - dstr_copy(&stream->path, dir); - dstr_replace(&stream->path, "\\", "/"); - if (dstr_end(&stream->path) != '/') - dstr_cat_ch(&stream->path, '/'); - dstr_cat(&stream->path, filename); - - char *slash = strrchr(stream->path.array, '/'); - if (slash) { - *slash = 0; - os_mkdirs(stream->path.array); - *slash = '/'; - } - - bfree(filename); - obs_data_release(settings); - - /* ---------------------------- */ + generate_filename(stream, &stream->path); os_atomic_set_bool(&stream->muxing, true); stream->mux_thread_joinable = pthread_create(&stream->mux_thread, NULL, From ce92f441b5cc33b3f5c7ee4d1b6d087a27d47e17 Mon Sep 17 00:00:00 2001 From: Norihiro Kamae Date: Fri, 1 Oct 2021 23:44:21 +0900 Subject: [PATCH 2/5] 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); From 0e81c66f6e5a71e3d9c6e771c981ebf5c9f0b802 Mon Sep 17 00:00:00 2001 From: Norihiro Kamae Date: Sun, 3 Oct 2021 19:06:46 +0900 Subject: [PATCH 3/5] UI: Add automatic file splitting This commit implements a new feature to split recordings in split files in Advanced output mode. These basic settings are implemented. - Enable/disable the feature. Default is disabled. - Select a type of the limit, time or size. - Specifies the limit in seconds or MiB. --- UI/data/locale/en-US.ini | 5 ++ UI/forms/OBSBasicSettings.ui | 99 ++++++++++++++++++++++++++++++++ UI/window-basic-main-outputs.cpp | 49 ++++++++++++++++ UI/window-basic-main-outputs.hpp | 1 + UI/window-basic-main.cpp | 17 +++++- UI/window-basic-main.hpp | 3 +- UI/window-basic-settings.cpp | 50 ++++++++++++++++ UI/window-basic-settings.hpp | 1 + 8 files changed, 222 insertions(+), 3 deletions(-) diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 536504507..b0698b6cd 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -941,6 +941,11 @@ Basic.Settings.Output.Adv.FFmpeg.AEncoderSettings="Audio Encoder Settings (if an Basic.Settings.Output.Adv.FFmpeg.MuxerSettings="Muxer Settings (if any)" Basic.Settings.Output.Adv.FFmpeg.GOPSize="Keyframe interval (frames)" Basic.Settings.Output.Adv.FFmpeg.IgnoreCodecCompat="Show all codecs (even if potentially incompatible)" +Basic.Settings.Output.EnableSplitFile="Automatic File Splitting" +Basic.Settings.Output.SplitFile.TypeTime="Split by Time" +Basic.Settings.Output.SplitFile.TypeSize="Split by Size" +Basic.Settings.Output.SplitFile.Time="Split Time" +Basic.Settings.Output.SplitFile.Size="Split Size" # Screenshot Screenshot="Screenshot Output" diff --git a/UI/forms/OBSBasicSettings.ui b/UI/forms/OBSBasicSettings.ui index 4b562a174..1eee870d7 100644 --- a/UI/forms/OBSBasicSettings.ui +++ b/UI/forms/OBSBasicSettings.ui @@ -2408,6 +2408,85 @@ + + + + + 0 + 0 + + + + Qt::RightToLeft + + + Basic.Settings.Output.EnableSplitFile + + + + + + + false + + + + Basic.Settings.Output.SplitFile.TypeTime + + + + + Basic.Settings.Output.SplitFile.TypeSize + + + + + + + + Basic.Settings.Output.SplitFile.Time + + + + + + + s + + + 5 + + + 21600 + + + 900 + + + + + + + Basic.Settings.Output.SplitFile.Size + + + + + + + MB + + + 20 + + + 8192 + + + 2048 + + + @@ -5675,6 +5754,10 @@ advOutRecUseRescale advOutRecRescale advOutMuxCustom + advOutSplitFile + advOutSplitFileType + advOutSplitFileTime + advOutSplitFileSize advOutFFType advOutFFRecPath advOutFFPathBrowse @@ -6198,5 +6281,21 @@ + + advOutSplitFile + toggled(bool) + advOutSplitFileType + setEnabled(bool) + + + 327 + 355 + + + 701 + 355 + + + diff --git a/UI/window-basic-main-outputs.cpp b/UI/window-basic-main-outputs.cpp index e8eb6f7c4..bd346b949 100644 --- a/UI/window-basic-main-outputs.cpp +++ b/UI/window-basic-main-outputs.cpp @@ -110,6 +110,20 @@ static void OBSRecordStopping(void *data, calldata_t *params) UNUSED_PARAMETER(params); } +static void OBSRecordFileChanged(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + const char *next_file = calldata_string(params, "next_file"); + + QString arg_last_file = + QString::fromUtf8(output->lastRecordingPath.c_str()); + + QMetaObject::invokeMethod(output->main, "RecordingFileChanged", + Q_ARG(QString, arg_last_file)); + + output->lastRecordingPath = next_file; +} + static void OBSStartReplayBuffer(void *data, calldata_t *params) { BasicOutputHandler *output = static_cast(data); @@ -1311,6 +1325,8 @@ AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) OBSStopRecording, this); recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); + recordFileChanged.Connect(obs_output_get_signal_handler(fileOutput), + "file_changed", OBSRecordFileChanged, this); } void AdvancedOutput::UpdateStreamSettings() @@ -1834,6 +1850,10 @@ bool AdvancedOutput::StartRecording() const char *filenameFormat; bool noSpace = false; bool overwriteIfExists = false; + bool splitFile; + const char *splitFileType; + int splitFileTime; + int splitFileSize; if (!useStreamEncoder) { if (!ffmpegOutput) { @@ -1863,6 +1883,8 @@ bool AdvancedOutput::StartRecording() ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + splitFile = config_get_bool(main->Config(), "AdvOut", + "RecSplitFile"); string strPath = GetRecordingFilename(path, recFormat, noSpace, overwriteIfExists, @@ -1873,6 +1895,33 @@ bool AdvancedOutput::StartRecording() obs_data_set_string(settings, ffmpegRecording ? "url" : "path", strPath.c_str()); + if (splitFile) { + splitFileType = config_get_string( + main->Config(), "AdvOut", "RecSplitFileType"); + splitFileTime = + (astrcmpi(splitFileType, "Time") == 0) + ? config_get_int(main->Config(), + "AdvOut", + "RecSplitFileTime") + : 0; + splitFileSize = + (astrcmpi(splitFileType, "Size") == 0) + ? config_get_int(main->Config(), + "AdvOut", + "RecSplitFileSize") + : 0; + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", filenameFormat); + obs_data_set_string(settings, "extension", recFormat); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_bool(settings, "allow_overwrite", + overwriteIfExists); + obs_data_set_int(settings, "max_time_sec", + splitFileTime); + obs_data_set_int(settings, "max_size_mb", + splitFileSize); + } + obs_output_update(fileOutput, settings); } diff --git a/UI/window-basic-main-outputs.hpp b/UI/window-basic-main-outputs.hpp index 242b26434..c94fcfbf3 100644 --- a/UI/window-basic-main-outputs.hpp +++ b/UI/window-basic-main-outputs.hpp @@ -32,6 +32,7 @@ struct BasicOutputHandler { OBSSignal streamDelayStarting; OBSSignal streamStopping; OBSSignal recordStopping; + OBSSignal recordFileChanged; OBSSignal replayBufferStopping; OBSSignal replayBufferSaved; diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 0517ae255..a006c38fd 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -1414,6 +1414,10 @@ bool OBSBasic::InitBasicConfigDefaults() config_set_default_uint(basicConfig, "AdvOut", "Track5Bitrate", 160); config_set_default_uint(basicConfig, "AdvOut", "Track6Bitrate", 160); + config_set_default_uint(basicConfig, "AdvOut", "RecSplitFileTime", 900); + config_set_default_uint(basicConfig, "AdvOut", "RecSplitFileSize", + 2048); + config_set_default_bool(basicConfig, "AdvOut", "RecRB", false); config_set_default_uint(basicConfig, "AdvOut", "RecRBTime", 20); config_set_default_int(basicConfig, "AdvOut", "RecRBSize", 512); @@ -6947,7 +6951,7 @@ void OBSBasic::StreamingStop(int code, QString last_error) SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); } -void OBSBasic::AutoRemux(QString input) +void OBSBasic::AutoRemux(QString input, bool no_show) { bool autoRemux = config_get_bool(Config(), "Video", "AutoRemux"); @@ -6979,7 +6983,8 @@ void OBSBasic::AutoRemux(QString input) output += "mp4"; OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); - remux->show(); + if (!no_show) + remux->show(); remux->AutoRemux(input, output); } @@ -7132,6 +7137,14 @@ void OBSBasic::RecordingStop(int code, QString last_error) UpdatePause(false); } +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + void OBSBasic::ShowReplayBufferPauseWarning() { auto msgBox = []() { diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 9b5b2db2d..2f984d021 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -630,6 +630,7 @@ public slots: void RecordingStart(); void RecordStopping(); void RecordingStop(int code, QString last_error); + void RecordingFileChanged(QString lastRecordingPath); void ShowReplayBufferPauseWarning(); void StartReplayBuffer(); @@ -797,7 +798,7 @@ private: static void HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed); - void AutoRemux(QString input); + void AutoRemux(QString input, bool no_show = false); void UpdatePause(bool activate = true); void UpdateReplayBuffer(bool activate = true); diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index 2a34e6283..523c1c599 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -457,6 +457,10 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->advOutRecUseRescale, CHECK_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advOutRecRescale, CBEDIT_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advOutMuxCustom, EDIT_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutSplitFile, CHECK_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutSplitFileType, COMBO_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutSplitFileTime, SCROLL_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutSplitFileSize, SCROLL_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advOutRecTrack1, CHECK_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advOutRecTrack2, CHECK_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advOutRecTrack3, CHECK_CHANGED, OUTPUTS_CHANGED); @@ -769,6 +773,10 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) this, SLOT(SimpleReplayBufferChanged())); connect(ui->simpleRBSecMax, SIGNAL(valueChanged(int)), this, SLOT(SimpleReplayBufferChanged())); + connect(ui->advOutSplitFile, SIGNAL(stateChanged(int)), this, + SLOT(AdvOutSplitFileChanged())); + connect(ui->advOutSplitFileType, SIGNAL(currentIndexChanged(int)), this, + SLOT(AdvOutSplitFileChanged())); connect(ui->advReplayBuf, SIGNAL(toggled(bool)), this, SLOT(AdvReplayBufferChanged())); connect(ui->advOutRecTrack1, SIGNAL(toggled(bool)), this, @@ -895,6 +903,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) ui->buttonBox->button(QDialogButtonBox::Cancel)->setIcon(QIcon()); SimpleRecordingQualityChanged(); + AdvOutSplitFileChanged(); UpdateAutomaticReplayBufferCheckboxes(); @@ -1901,6 +1910,14 @@ void OBSBasicSettings::LoadAdvOutputRecordingSettings() config_get_string(main->Config(), "AdvOut", "RecMuxerCustom"); int tracks = config_get_int(main->Config(), "AdvOut", "RecTracks"); int flvTrack = config_get_int(main->Config(), "AdvOut", "FLVTrack"); + bool splitFile = + config_get_bool(main->Config(), "AdvOut", "RecSplitFile"); + const char *splitFileType = + config_get_string(main->Config(), "AdvOut", "RecSplitFileType"); + int splitFileTime = + config_get_int(main->Config(), "AdvOut", "RecSplitFileTime"); + int splitFileSize = + config_get_int(main->Config(), "AdvOut", "RecSplitFileSize"); int typeIndex = (astrcmpi(type, "FFmpeg") == 0) ? 1 : 0; ui->advOutRecType->setCurrentIndex(typeIndex); @@ -1920,6 +1937,12 @@ void OBSBasicSettings::LoadAdvOutputRecordingSettings() ui->advOutRecTrack5->setChecked(tracks & (1 << 4)); ui->advOutRecTrack6->setChecked(tracks & (1 << 5)); + idx = (astrcmpi(splitFileType, "Size") == 0) ? 1 : 0; + ui->advOutSplitFile->setChecked(splitFile); + ui->advOutSplitFileType->setCurrentIndex(idx); + ui->advOutSplitFileTime->setValue(splitFileTime); + ui->advOutSplitFileSize->setValue(splitFileSize); + switch (flvTrack) { case 1: ui->flvTrack1->setChecked(true); @@ -3378,6 +3401,14 @@ static inline const char *RecTypeFromIdx(int idx) return "Standard"; } +static inline const char *SplitFileTypeFromIdx(int idx) +{ + if (idx == 1) + return "Size"; + else + return "Time"; +} + static void WriteJsonData(OBSPropertiesView *view, const char *path) { char full_path[512]; @@ -3513,6 +3544,12 @@ void OBSBasicSettings::SaveOutputSettings() SaveCheckBox(ui->advOutRecUseRescale, "AdvOut", "RecRescale"); SaveCombo(ui->advOutRecRescale, "AdvOut", "RecRescaleRes"); SaveEdit(ui->advOutMuxCustom, "AdvOut", "RecMuxerCustom"); + SaveCheckBox(ui->advOutSplitFile, "AdvOut", "RecSplitFile"); + config_set_string( + main->Config(), "AdvOut", "RecSplitFileType", + SplitFileTypeFromIdx(ui->advOutSplitFileType->currentIndex())); + SaveSpinBox(ui->advOutSplitFileTime, "AdvOut", "RecSplitFileTime"); + SaveSpinBox(ui->advOutSplitFileSize, "AdvOut", "RecSplitFileSize"); config_set_int( main->Config(), "AdvOut", "RecTracks", @@ -4414,6 +4451,19 @@ void OBSBasicSettings::AdvancedChanged() } } +void OBSBasicSettings::AdvOutSplitFileChanged() +{ + bool splitFile = ui->advOutSplitFile->isChecked(); + int splitFileType = splitFile ? ui->advOutSplitFileType->currentIndex() + : -1; + + ui->advOutSplitFileType->setEnabled(splitFile); + ui->advOutSplitFileTimeLabel->setVisible(splitFileType == 0); + ui->advOutSplitFileTime->setVisible(splitFileType == 0); + ui->advOutSplitFileSizeLabel->setVisible(splitFileType == 1); + ui->advOutSplitFileSize->setVisible(splitFileType == 1); +} + void OBSBasicSettings::AdvOutRecCheckWarnings() { auto Checked = [](QCheckBox *box) { return box->isChecked() ? 1 : 0; }; diff --git a/UI/window-basic-settings.hpp b/UI/window-basic-settings.hpp index b27c9e143..73610d9ce 100644 --- a/UI/window-basic-settings.hpp +++ b/UI/window-basic-settings.hpp @@ -381,6 +381,7 @@ private slots: void UpdateAutomaticReplayBufferCheckboxes(); + void AdvOutSplitFileChanged(); void AdvOutRecCheckWarnings(); void SimpleRecordingQualityChanged(); From 550b1331acb002b34792a3fc48bf27192bbe8aa0 Mon Sep 17 00:00:00 2001 From: Norihiro Kamae Date: Fri, 24 Dec 2021 15:59:26 +0900 Subject: [PATCH 4/5] obs-ffmpeg, UI: Reset timestamps at splitting file This commit adds a setting to reset timestamps when splitting files. Some NLEs cannot handle video files whose starting timestamp is not zero. Default is enabed. --- UI/data/locale/en-US.ini | 1 + UI/forms/OBSBasicSettings.ui | 11 +++++++ UI/window-basic-main-outputs.cpp | 6 ++++ UI/window-basic-main.cpp | 2 ++ UI/window-basic-settings.cpp | 7 +++++ plugins/obs-ffmpeg/obs-ffmpeg-mux.c | 48 +++++++++++++++++++++++++++++ plugins/obs-ffmpeg/obs-ffmpeg-mux.h | 7 +++++ 7 files changed, 82 insertions(+) diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index b0698b6cd..526dd0229 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -946,6 +946,7 @@ Basic.Settings.Output.SplitFile.TypeTime="Split by Time" Basic.Settings.Output.SplitFile.TypeSize="Split by Size" Basic.Settings.Output.SplitFile.Time="Split Time" Basic.Settings.Output.SplitFile.Size="Split Size" +Basic.Settings.Output.SplitFile.ResetTimestamps="Reset timestamps at the beginning of each split file" # Screenshot Screenshot="Screenshot Output" diff --git a/UI/forms/OBSBasicSettings.ui b/UI/forms/OBSBasicSettings.ui index 1eee870d7..02a8bf258 100644 --- a/UI/forms/OBSBasicSettings.ui +++ b/UI/forms/OBSBasicSettings.ui @@ -2487,6 +2487,16 @@ + + + + Basic.Settings.Output.SplitFile.ResetTimestamps + + + true + + + @@ -5758,6 +5768,7 @@ advOutSplitFileType advOutSplitFileTime advOutSplitFileSize + advOutSplitFileRstTS advOutFFType advOutFFRecPath advOutFFPathBrowse diff --git a/UI/window-basic-main-outputs.cpp b/UI/window-basic-main-outputs.cpp index bd346b949..033062378 100644 --- a/UI/window-basic-main-outputs.cpp +++ b/UI/window-basic-main-outputs.cpp @@ -1854,6 +1854,7 @@ bool AdvancedOutput::StartRecording() const char *splitFileType; int splitFileTime; int splitFileSize; + bool splitFileResetTimestamps; if (!useStreamEncoder) { if (!ffmpegOutput) { @@ -1910,6 +1911,9 @@ bool AdvancedOutput::StartRecording() "AdvOut", "RecSplitFileSize") : 0; + splitFileResetTimestamps = + config_get_bool(main->Config(), "AdvOut", + "RecSplitFileResetTimestamps"); obs_data_set_string(settings, "directory", path); obs_data_set_string(settings, "format", filenameFormat); obs_data_set_string(settings, "extension", recFormat); @@ -1920,6 +1924,8 @@ bool AdvancedOutput::StartRecording() splitFileTime); obs_data_set_int(settings, "max_size_mb", splitFileSize); + obs_data_set_bool(settings, "reset_timestamps", + splitFileResetTimestamps); } obs_output_update(fileOutput, settings); diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index a006c38fd..f02371f3b 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -1417,6 +1417,8 @@ bool OBSBasic::InitBasicConfigDefaults() config_set_default_uint(basicConfig, "AdvOut", "RecSplitFileTime", 900); config_set_default_uint(basicConfig, "AdvOut", "RecSplitFileSize", 2048); + config_set_default_bool(basicConfig, "AdvOut", + "RecSplitFileResetTimestamps", true); config_set_default_bool(basicConfig, "AdvOut", "RecRB", false); config_set_default_uint(basicConfig, "AdvOut", "RecRBTime", 20); diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index 523c1c599..8420f6efb 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -461,6 +461,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->advOutSplitFileType, COMBO_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advOutSplitFileTime, SCROLL_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advOutSplitFileSize, SCROLL_CHANGED, OUTPUTS_CHANGED); + HookWidget(ui->advOutSplitFileRstTS, CHECK_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advOutRecTrack1, CHECK_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advOutRecTrack2, CHECK_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->advOutRecTrack3, CHECK_CHANGED, OUTPUTS_CHANGED); @@ -1918,6 +1919,8 @@ void OBSBasicSettings::LoadAdvOutputRecordingSettings() config_get_int(main->Config(), "AdvOut", "RecSplitFileTime"); int splitFileSize = config_get_int(main->Config(), "AdvOut", "RecSplitFileSize"); + bool splitFileResetTimestamps = config_get_bool( + main->Config(), "AdvOut", "RecSplitFileResetTimestamps"); int typeIndex = (astrcmpi(type, "FFmpeg") == 0) ? 1 : 0; ui->advOutRecType->setCurrentIndex(typeIndex); @@ -1942,6 +1945,7 @@ void OBSBasicSettings::LoadAdvOutputRecordingSettings() ui->advOutSplitFileType->setCurrentIndex(idx); ui->advOutSplitFileTime->setValue(splitFileTime); ui->advOutSplitFileSize->setValue(splitFileSize); + ui->advOutSplitFileRstTS->setChecked(splitFileResetTimestamps); switch (flvTrack) { case 1: @@ -3550,6 +3554,8 @@ void OBSBasicSettings::SaveOutputSettings() SplitFileTypeFromIdx(ui->advOutSplitFileType->currentIndex())); SaveSpinBox(ui->advOutSplitFileTime, "AdvOut", "RecSplitFileTime"); SaveSpinBox(ui->advOutSplitFileSize, "AdvOut", "RecSplitFileSize"); + SaveCheckBox(ui->advOutSplitFileRstTS, "AdvOut", + "RecSplitFileResetTimestamps"); config_set_int( main->Config(), "AdvOut", "RecTracks", @@ -4462,6 +4468,7 @@ void OBSBasicSettings::AdvOutSplitFileChanged() ui->advOutSplitFileTime->setVisible(splitFileType == 0); ui->advOutSplitFileSizeLabel->setVisible(splitFileType == 1); ui->advOutSplitFileSize->setVisible(splitFileType == 1); + ui->advOutSplitFileRstTS->setVisible(splitFile); } void OBSBasicSettings::AdvOutRecCheckWarnings() diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-mux.c b/plugins/obs-ffmpeg/obs-ffmpeg-mux.c index 18eae54d7..9bd0f5b0c 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-mux.c +++ b/plugins/obs-ffmpeg/obs-ffmpeg-mux.c @@ -309,6 +309,35 @@ static void set_file_not_readable_error(struct ffmpeg_muxer *stream, obs_data_release(settings); } +inline static void ts_offset_clear(struct ffmpeg_muxer *stream) +{ + stream->found_video = false; + stream->video_pts_offset = 0; + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + stream->found_audio[i] = false; + stream->audio_dts_offsets[i] = 0; + } +} + +inline static void ts_offset_update(struct ffmpeg_muxer *stream, + struct encoder_packet *packet) +{ + if (packet->type == OBS_ENCODER_VIDEO) { + if (!stream->found_video) { + stream->video_pts_offset = packet->pts; + stream->found_video = true; + } + return; + } + + if (stream->found_audio[packet->track_idx]) + return; + + stream->audio_dts_offsets[packet->track_idx] = packet->dts; + stream->found_audio[packet->track_idx] = true; +} + static bool ffmpeg_mux_start(void *data) { struct ffmpeg_muxer *stream = data; @@ -328,6 +357,7 @@ static bool ffmpeg_mux_start(void *data) return false; path = obs_service_get_url(service); stream->split_file = false; + stream->reset_timestamps = false; } else { path = obs_data_get_string(settings, "path"); @@ -337,12 +367,16 @@ static bool ffmpeg_mux_start(void *data) (1024 * 1024); stream->split_file = stream->max_time > 0 || stream->max_size > 0; + stream->reset_timestamps = + obs_data_get_bool(settings, "reset_timestamps"); stream->allow_overwrite = obs_data_get_bool(settings, "allow_overwrite"); stream->cur_size = 0; stream->sent_headers = false; } + ts_offset_clear(stream); + if (!stream->is_network) { /* ensure output path is writable to avoid generic error * message. @@ -545,6 +579,16 @@ bool write_packet(struct ffmpeg_muxer *stream, struct encoder_packet *packet) : FFM_PACKET_AUDIO, .keyframe = packet->keyframe}; + if (stream->split_file && stream->reset_timestamps) { + if (is_video) { + info.dts -= stream->video_pts_offset; + info.pts -= stream->video_pts_offset; + } else { + info.dts -= stream->audio_dts_offsets[info.index]; + info.pts -= stream->audio_dts_offsets[info.index]; + } + } + ret = os_process_pipe_write(stream->pipe, (const uint8_t *)&info, sizeof(info)); if (ret != sizeof(info)) { @@ -682,6 +726,7 @@ static bool prepare_split_file(struct ffmpeg_muxer *stream, stream->cur_size = 0; stream->cur_time = packet->dts_usec; + ts_offset_clear(stream); return true; } @@ -721,6 +766,9 @@ static void ffmpeg_mux_data(void *data, struct encoder_packet *packet) } } + if (stream->split_file && stream->reset_timestamps) + ts_offset_update(stream, packet); + write_packet(stream, packet); } diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-mux.h b/plugins/obs-ffmpeg/obs-ffmpeg-mux.h index d0de39272..7d8a55add 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-mux.h +++ b/plugins/obs-ffmpeg/obs-ffmpeg-mux.h @@ -37,6 +37,12 @@ struct ffmpeg_muxer { volatile bool muxing; DARRAY(struct encoder_packet) mux_packets; + /* split file */ + bool found_video; + bool found_audio[MAX_AUDIO_MIXES]; + int64_t video_pts_offset; + int64_t audio_dts_offsets[MAX_AUDIO_MIXES]; + /* these are accessed both by replay buffer and by HLS */ pthread_t mux_thread; bool mux_thread_joinable; @@ -54,6 +60,7 @@ struct ffmpeg_muxer { bool is_network; bool split_file; + bool reset_timestamps; bool allow_overwrite; }; From 61c0a76fc0d0306bcddb6ca6b56600855cdcd4d4 Mon Sep 17 00:00:00 2001 From: Norihiro Kamae Date: Sat, 11 Dec 2021 21:46:10 +0900 Subject: [PATCH 5/5] obs-ffmpeg: Split file by PTS instead of DTS If B-frame is enabled in video encoder, video packets have different PTS and DTS, however audio packets do not. That caused audio packets at the splitting point goes to a different file and audio glitch appeared in an NLE. --- plugins/obs-ffmpeg/obs-ffmpeg-mux.c | 57 ++++++++++++++++++++++++++++- plugins/obs-ffmpeg/obs-ffmpeg-mux.h | 1 + 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-mux.c b/plugins/obs-ffmpeg/obs-ffmpeg-mux.c index 9bd0f5b0c..b2f918147 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-mux.c +++ b/plugins/obs-ffmpeg/obs-ffmpeg-mux.c @@ -320,6 +320,11 @@ inline static void ts_offset_clear(struct ffmpeg_muxer *stream) } } +static inline int64_t packet_pts_usec(struct encoder_packet *packet) +{ + return packet->pts * 1000000 / packet->timebase_den; +} + inline static void ts_offset_update(struct ffmpeg_muxer *stream, struct encoder_packet *packet) { @@ -731,6 +736,19 @@ static bool prepare_split_file(struct ffmpeg_muxer *stream, return true; } +static inline bool has_audio(struct ffmpeg_muxer *stream) +{ + return !!obs_output_get_audio_encoder(stream->output, 0); +} + +static void push_back_packet(struct darray *packets, + struct encoder_packet *packet) +{ + struct encoder_packet pkt; + obs_encoder_packet_ref(&pkt, packet); + darray_push_back(sizeof(pkt), packets, &pkt); +} + static void ffmpeg_mux_data(void *data, struct encoder_packet *packet) { struct ffmpeg_muxer *stream = data; @@ -744,9 +762,31 @@ 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)) + if (stream->split_file && stream->mux_packets.num) { + int64_t pts_usec = packet_pts_usec(packet); + struct encoder_packet *first_pkt = stream->mux_packets.array; + int64_t first_pts_usec = packet_pts_usec(first_pkt); + + if (pts_usec >= first_pts_usec) { + if (packet->type != OBS_ENCODER_AUDIO) { + push_back_packet(&stream->mux_packets.da, + packet); + return; + } + + if (!prepare_split_file(stream, first_pkt)) + return; + stream->split_file_ready = true; + } + } else if (stream->split_file && should_split(stream, packet)) { + if (has_audio(stream)) { + push_back_packet(&stream->mux_packets.da, packet); return; + } else { + if (!prepare_split_file(stream, packet)) + return; + stream->split_file_ready = true; + } } if (!stream->sent_headers) { @@ -766,6 +806,19 @@ static void ffmpeg_mux_data(void *data, struct encoder_packet *packet) } } + if (stream->split_file && stream->split_file_ready) { + for (size_t i = 0; i < stream->mux_packets.num; i++) { + struct encoder_packet *pkt = + &stream->mux_packets.array[i]; + if (stream->reset_timestamps) + ts_offset_update(stream, pkt); + write_packet(stream, pkt); + obs_encoder_packet_release(pkt); + } + da_free(stream->mux_packets); + stream->split_file_ready = false; + } + if (stream->split_file && stream->reset_timestamps) ts_offset_update(stream, packet); diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-mux.h b/plugins/obs-ffmpeg/obs-ffmpeg-mux.h index 7d8a55add..1dcaa2c6e 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-mux.h +++ b/plugins/obs-ffmpeg/obs-ffmpeg-mux.h @@ -42,6 +42,7 @@ struct ffmpeg_muxer { bool found_audio[MAX_AUDIO_MIXES]; int64_t video_pts_offset; int64_t audio_dts_offsets[MAX_AUDIO_MIXES]; + bool split_file_ready; /* these are accessed both by replay buffer and by HLS */ pthread_t mux_thread;