diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index c6ddf2d86..fd074879f 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -956,6 +956,12 @@ 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" +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 a19167f9d..6e6ab61cf 100644 --- a/UI/forms/OBSBasicSettings.ui +++ b/UI/forms/OBSBasicSettings.ui @@ -2418,6 +2418,95 @@ + + + + + 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 + + + + + + + Basic.Settings.Output.SplitFile.ResetTimestamps + + + true + + + @@ -5784,6 +5873,11 @@ advOutRecUseRescale advOutRecRescale advOutMuxCustom + advOutSplitFile + advOutSplitFileType + advOutSplitFileTime + advOutSplitFileSize + advOutSplitFileRstTS advOutFFType advOutFFRecPath advOutFFPathBrowse @@ -6307,5 +6401,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 d4351f7f6..253d8d320 100644 --- a/UI/window-basic-main-outputs.cpp +++ b/UI/window-basic-main-outputs.cpp @@ -109,6 +109,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); @@ -1300,6 +1314,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() @@ -1823,6 +1839,11 @@ bool AdvancedOutput::StartRecording() const char *filenameFormat; bool noSpace = false; bool overwriteIfExists = false; + bool splitFile; + const char *splitFileType; + int splitFileTime; + int splitFileSize; + bool splitFileResetTimestamps; if (!useStreamEncoder) { if (!ffmpegOutput) { @@ -1852,6 +1873,8 @@ bool AdvancedOutput::StartRecording() ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + splitFile = config_get_bool(main->Config(), "AdvOut", + "RecSplitFile"); string strPath = GetRecordingFilename(path, recFormat, noSpace, overwriteIfExists, @@ -1862,6 +1885,38 @@ 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; + 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); + 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_data_set_bool(settings, "reset_timestamps", + splitFileResetTimestamps); + } + obs_output_update(fileOutput, settings); } diff --git a/UI/window-basic-main-outputs.hpp b/UI/window-basic-main-outputs.hpp index 2393d38d4..88e5d962b 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 727dfbdac..ddb566c88 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -1424,6 +1424,12 @@ 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", + "RecSplitFileResetTimestamps", true); + config_set_default_bool(basicConfig, "AdvOut", "RecRB", false); config_set_default_uint(basicConfig, "AdvOut", "RecRBTime", 20); config_set_default_int(basicConfig, "AdvOut", "RecRBSize", 512); @@ -7055,7 +7061,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"); @@ -7087,7 +7093,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); } @@ -7240,6 +7247,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 ecb5742aa..853389dec 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -639,6 +639,7 @@ public slots: void RecordingStart(); void RecordStopping(); void RecordingStop(int code, QString last_error); + void RecordingFileChanged(QString lastRecordingPath); void ShowReplayBufferPauseWarning(); void StartReplayBuffer(); @@ -807,7 +808,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 f9e004238..f7940828e 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -459,6 +459,11 @@ 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->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); @@ -781,6 +786,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, @@ -907,6 +916,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) ui->buttonBox->button(QDialogButtonBox::Cancel)->setIcon(QIcon()); SimpleRecordingQualityChanged(); + AdvOutSplitFileChanged(); UpdateAutomaticReplayBufferCheckboxes(); @@ -1964,6 +1974,16 @@ 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"); + bool splitFileResetTimestamps = config_get_bool( + main->Config(), "AdvOut", "RecSplitFileResetTimestamps"); int typeIndex = (astrcmpi(type, "FFmpeg") == 0) ? 1 : 0; ui->advOutRecType->setCurrentIndex(typeIndex); @@ -1983,6 +2003,13 @@ 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); + ui->advOutSplitFileRstTS->setChecked(splitFileResetTimestamps); + switch (flvTrack) { case 1: ui->flvTrack1->setChecked(true); @@ -3416,6 +3443,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]; @@ -3551,6 +3586,14 @@ 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"); + SaveCheckBox(ui->advOutSplitFileRstTS, "AdvOut", + "RecSplitFileResetTimestamps"); config_set_int( main->Config(), "AdvOut", "RecTracks", @@ -4544,6 +4587,20 @@ 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); + ui->advOutSplitFileRstTS->setVisible(splitFile); +} + 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 1ced9f5bb..374d12a55 100644 --- a/UI/window-basic-settings.hpp +++ b/UI/window-basic-settings.hpp @@ -393,6 +393,7 @@ private slots: void UpdateAutomaticReplayBufferCheckboxes(); + void AdvOutSplitFileChanged(); void AdvOutRecCheckWarnings(); void SimpleRecordingQualityChanged(); diff --git a/plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.c b/plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.c index 3ce1e7977..1ebf50a57 100644 --- a/plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.c +++ b/plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.c @@ -825,6 +825,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 @@ -836,6 +865,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; @@ -868,6 +898,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) { @@ -879,6 +915,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 01f51c413..4c98e864d 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; } @@ -315,6 +318,40 @@ 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; + } +} + +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) +{ + 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; @@ -333,10 +370,27 @@ static bool ffmpeg_mux_start(void *data) if (!service) return false; path = obs_service_get_url(service); + stream->split_file = false; + stream->reset_timestamps = 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->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. @@ -468,6 +522,64 @@ static void signal_failure(struct ffmpeg_muxer *stream) os_atomic_set_bool(&stream->capturing, false); } +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"); + + 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 = '/'; + } + + if (!overwrite) + find_best_filename(dst, space); + + 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; @@ -481,6 +593,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)) { @@ -497,6 +619,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; } @@ -542,6 +668,96 @@ 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; + ts_offset_clear(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; @@ -555,11 +771,41 @@ static void ffmpeg_mux_data(void *data, struct encoder_packet *packet) return; } + 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) { if (!send_headers(stream)) return; stream->sent_headers = true; + + if (stream->split_file) + stream->cur_time = packet->dts_usec; } if (stopping(stream)) { @@ -569,6 +815,22 @@ 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); + write_packet(stream, packet); } @@ -918,34 +1180,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, 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..1dcaa2c6e 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-mux.h +++ b/plugins/obs-ffmpeg/obs-ffmpeg-mux.h @@ -24,17 +24,26 @@ 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; 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]; + bool split_file_ready; + /* these are accessed both by replay buffer and by HLS */ pthread_t mux_thread; bool mux_thread_joinable; @@ -51,6 +60,9 @@ struct ffmpeg_muxer { int64_t last_dts_usec; bool is_network; + bool split_file; + bool reset_timestamps; + bool allow_overwrite; }; bool stopping(struct ffmpeg_muxer *stream);