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