diff --git a/obs/data/locale/en-US.ini b/obs/data/locale/en-US.ini
index 99c965038..88a2de719 100644
--- a/obs/data/locale/en-US.ini
+++ b/obs/data/locale/en-US.ini
@@ -404,6 +404,8 @@ Basic.Settings.Output.Adv.Recording.Type="Type"
Basic.Settings.Output.Adv.Recording.Type.Standard="Standard"
Basic.Settings.Output.Adv.Recording.Type.FFmpegOutput="Custom Output (FFmpeg)"
Basic.Settings.Output.Adv.Recording.UseStreamEncoder="(Use stream encoder)"
+Basic.Settings.Output.Adv.Recording.Filename="Filename Formatting"
+Basic.Settings.Output.Adv.Recording.OverwriteIfExists="Overwrite if file exists"
Basic.Settings.Output.Adv.FFmpeg.Type="FFmpeg Output Type"
Basic.Settings.Output.Adv.FFmpeg.Type.URL="Output to URL"
Basic.Settings.Output.Adv.FFmpeg.Type.RecordToFile="Output to File"
@@ -424,6 +426,12 @@ Basic.Settings.Output.Adv.FFmpeg.AEncoder="Audio Encoder"
Basic.Settings.Output.Adv.FFmpeg.AEncoderSettings="Audio Encoder Settings (if any)"
Basic.Settings.Output.Adv.FFmpeg.MuxerSettings="Muxer Settings (if any)"
+# basic mode 'output' settings - advanced section - recording subsection - completer
+FilenameFormatting.completer="%yyyy-%mm-%dd %hh-%mm-%ss\n%yy-%mm-%dd %hh-%mm-%ss\n%Y-%m-%d %H-%M-%S\n%y-%m-%d %H-%M-%S\n%a %Y-%m-%d %H-%M-%S\n%A %Y-%m-%d %H-%M-%S\n%Y-%b-%d %H-%M-%S\n%Y-%B-%d %H-%M-%S\n%Y-%m-%d %I-%M-%S-%p\n%Y-%m-%d %H-%M-%S-%z\n%Y-%m-%d %H-%M-%S-%Z"
+
+# basic mode 'output' settings - advanced section - recording subsection - TT
+FilenameFormatting.TT="%yyyy Year, four digits\n%yy Year, last two digits (00-99)\n%mm Month as a decimal number (01-12)\n%dd Day of the month, zero-padded (01-31)\n%hh Hour in 24h format (00-23)\n%mm Minute (00-59)\n%ss Second (00-61)\n%% A % sign\n%a Abbreviated weekday name\n%A Full weekday name\n%b Abbreviated month name\n%B Full month name\n%d Day of the month, zero-padded (01-31)\n%H Hour in 24h format (00-23)\n%I Hour in 12h format (01-12)\n%m Month as a decimal number (01-12)\n%M Minute (00-59)\n%p AM or PM designation\n%S Second (00-61)\n%y Year, last two digits (00-99)\n%Y Year\n%z ISO 8601 offset from UTC or timezone\n name or abbreviation\n%Z Timezone name or abbreviation\n"
+
# basic mode 'video' settings
Basic.Settings.Video="Video"
Basic.Settings.Video.Adapter="Video Adapter:"
diff --git a/obs/forms/OBSBasicSettings.ui b/obs/forms/OBSBasicSettings.ui
index 4b8919c12..fae897a91 100644
--- a/obs/forms/OBSBasicSettings.ui
+++ b/obs/forms/OBSBasicSettings.ui
@@ -2953,6 +2953,32 @@
+ -
+
+
+ Basic.Settings.Output.Adv.Recording
+
+
+
-
+
+
+ Basic.Settings.Output.Adv.Recording.Filename
+
+
+
+ -
+
+
+ -
+
+
+ Basic.Settings.Output.Adv.Recording.OverwriteIfExists
+
+
+
+
+
+
-
diff --git a/obs/window-basic-main-outputs.cpp b/obs/window-basic-main-outputs.cpp
index 7149df493..e8711dde4 100644
--- a/obs/window-basic-main-outputs.cpp
+++ b/obs/window-basic-main-outputs.cpp
@@ -75,6 +75,36 @@ static void OBSStopRecording(void *data, calldata_t *params)
UNUSED_PARAMETER(params);
}
+static void FindBestFilename(string &strPath, bool noSpace)
+{
+ int num = 2;
+
+ if (!os_file_exists(strPath.c_str()))
+ return;
+
+ const char *ext = strrchr(strPath.c_str(), '.');
+ if (!ext)
+ return;
+
+ int extStart = int(ext - strPath.c_str());
+ for (;;) {
+ string testPath = strPath;
+ string numStr;
+
+ numStr = noSpace ? "_" : " (";
+ numStr += to_string(num++);
+ if (!noSpace)
+ numStr += ")";
+
+ testPath.insert(extStart, numStr);
+
+ if (!os_file_exists(testPath.c_str())) {
+ strPath = testPath;
+ break;
+ }
+ }
+}
+
/* ------------------------------------------------------------------------ */
static bool CreateAACEncoder(OBSEncoder &res, string &id, int bitrate,
@@ -431,6 +461,10 @@ bool SimpleOutput::StartRecording()
"MuxerCustom");
bool noSpace = config_get_bool(main->Config(), "SimpleOutput",
"FileNameWithoutSpace");
+ const char *filenameFormat = config_get_string(main->Config(), "Output",
+ "FilenameFormatting");
+ bool overwriteIfExists = config_get_bool(main->Config(), "Output",
+ "OverwriteIfExists");
os_dir_t *dir = path ? os_opendir(path) : nullptr;
@@ -450,8 +484,10 @@ bool SimpleOutput::StartRecording()
if (lastChar != '/' && lastChar != '\\')
strPath += "/";
- strPath += GenerateTimeDateFilename(ffmpegOutput ? "avi" : format,
- noSpace);
+ strPath += GenerateSpecifiedFilename(ffmpegOutput ? "avi" : format,
+ noSpace, filenameFormat);
+ if (!overwriteIfExists)
+ FindBestFilename(strPath, noSpace);
SetupOutputs();
@@ -932,8 +968,10 @@ bool AdvancedOutput::StartStreaming(obs_service_t *service)
bool AdvancedOutput::StartRecording()
{
const char *path;
- const char *format;
+ const char *recFormat;
+ const char *filenameFormat;
bool noSpace = false;
+ bool overwriteIfExists = false;
if (!useStreamEncoder) {
if (!ffmpegOutput) {
@@ -951,8 +989,12 @@ bool AdvancedOutput::StartRecording()
if (!ffmpegOutput || ffmpegRecording) {
path = config_get_string(main->Config(), "AdvOut",
ffmpegRecording ? "FFFilePath" : "RecFilePath");
- format = config_get_string(main->Config(), "AdvOut",
+ recFormat = config_get_string(main->Config(), "AdvOut",
ffmpegRecording ? "FFExtension" : "RecFormat");
+ filenameFormat = config_get_string(main->Config(), "Output",
+ "FilenameFormatting");
+ overwriteIfExists = config_get_bool(main->Config(), "Output",
+ "OverwriteIfExists");
noSpace = config_get_bool(main->Config(), "AdvOut",
ffmpegRecording ?
"FFFileNameWithoutSpace" :
@@ -976,7 +1018,10 @@ bool AdvancedOutput::StartRecording()
if (lastChar != '/' && lastChar != '\\')
strPath += "/";
- strPath += GenerateTimeDateFilename(format, noSpace);
+ strPath += GenerateSpecifiedFilename(recFormat, noSpace,
+ filenameFormat);
+ if (!overwriteIfExists)
+ FindBestFilename(strPath, noSpace);
obs_data_t *settings = obs_data_create();
obs_data_set_string(settings,
diff --git a/obs/window-basic-main.cpp b/obs/window-basic-main.cpp
index 23cc3f4a7..eaba4c3ea 100644
--- a/obs/window-basic-main.cpp
+++ b/obs/window-basic-main.cpp
@@ -749,6 +749,9 @@ bool OBSBasic::InitBasicConfigDefaults()
config_set_default_uint (basicConfig, "Video", "BaseCX", cx);
config_set_default_uint (basicConfig, "Video", "BaseCY", cy);
+ config_set_default_string(basicConfig, "Output", "FilenameFormatting",
+ "%yyyy-%mm-%dd %hh-%mm-%ss");
+
config_set_default_bool (basicConfig, "Output", "DelayEnable", false);
config_set_default_uint (basicConfig, "Output", "DelaySec", 20);
config_set_default_bool (basicConfig, "Output", "DelayPreserve", true);
diff --git a/obs/window-basic-settings.cpp b/obs/window-basic-settings.cpp
index b80306a0a..86d459330 100644
--- a/obs/window-basic-settings.cpp
+++ b/obs/window-basic-settings.cpp
@@ -22,6 +22,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -347,6 +348,8 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
HookWidget(ui->colorRange, COMBO_CHANGED, ADV_CHANGED);
HookWidget(ui->disableOSXVSync, CHECK_CHANGED, ADV_CHANGED);
HookWidget(ui->resetOSXVSync, CHECK_CHANGED, ADV_CHANGED);
+ HookWidget(ui->filenameFormatting, EDIT_CHANGED, ADV_CHANGED);
+ HookWidget(ui->overwriteIfExists, CHECK_CHANGED, ADV_CHANGED);
HookWidget(ui->streamDelayEnable, CHECK_CHANGED, ADV_CHANGED);
HookWidget(ui->streamDelaySec, SCROLL_CHANGED, ADV_CHANGED);
HookWidget(ui->streamDelayPreserve, CHECK_CHANGED, ADV_CHANGED);
@@ -1128,6 +1131,14 @@ void OBSBasicSettings::LoadAdvOutputStreamingSettings()
ui->advOutRescale->setEnabled(rescale);
ui->advOutRescale->setCurrentText(rescaleRes);
+ QStringList specList = QTStr("FilenameFormatting.completer").split(
+ QRegularExpression("\n"));
+ QCompleter *specCompleter = new QCompleter(specList);
+ specCompleter->setCaseSensitivity(Qt::CaseSensitive);
+ specCompleter->setFilterMode(Qt::MatchContains);
+ ui->filenameFormatting->setCompleter(specCompleter);
+ ui->filenameFormatting->setToolTip(QTStr("FilenameFormatting.TT"));
+
switch (trackIndex) {
case 1: ui->advOutTrack1->setChecked(true); break;
case 2: ui->advOutTrack2->setChecked(true); break;
@@ -1664,11 +1675,18 @@ void OBSBasicSettings::LoadAdvancedSettings()
"RetryDelay");
int maxRetries = config_get_int(main->Config(), "Output",
"MaxRetries");
+ const char *filename = config_get_string(main->Config(), "Output",
+ "FilenameFormatting");
+ bool overwriteIfExists = config_get_bool(main->Config(), "Output",
+ "OverwriteIfExists");
loading = true;
LoadRendererList();
+ ui->filenameFormatting->setText(filename);
+ ui->overwriteIfExists->setChecked(overwriteIfExists);
+
ui->reconnectEnable->setChecked(reconnect);
ui->reconnectRetryDelay->setValue(retryDelay);
ui->reconnectMaxRetries->setValue(maxRetries);
@@ -2110,6 +2128,8 @@ void OBSBasicSettings::SaveAdvancedSettings()
SaveCombo(ui->colorFormat, "Video", "ColorFormat");
SaveCombo(ui->colorSpace, "Video", "ColorSpace");
SaveComboData(ui->colorRange, "Video", "ColorRange");
+ SaveEdit(ui->filenameFormatting, "Output", "FilenameFormatting");
+ SaveCheckBox(ui->overwriteIfExists, "Output", "OverwriteIfExists");
SaveCheckBox(ui->streamDelayEnable, "Output", "DelayEnable");
SaveSpinBox(ui->streamDelaySec, "Output", "DelaySec");
SaveCheckBox(ui->streamDelayPreserve, "Output", "DelayPreserve");
@@ -2690,6 +2710,23 @@ void OBSBasicSettings::RecalcOutputResPixels(const char *resText)
}
}
+
+void OBSBasicSettings::on_filenameFormatting_textEdited(const QString &text)
+{
+#ifdef __APPLE__
+ size_t invalidLocation =
+ text.toStdString().find_first_of(":/\\");
+#elif _WIN32
+ size_t invalidLocation =
+ text.toStdString().find_first_of("<>:\"/\\|?*");
+#else
+ size_t invalidLocation = text.toStdString().find_first_of("/");
+#endif
+
+ if (invalidLocation != string::npos)
+ ui->filenameFormatting->backspace();
+}
+
void OBSBasicSettings::on_outputResolution_editTextChanged(const QString &text)
{
if (!loading)
diff --git a/obs/window-basic-settings.hpp b/obs/window-basic-settings.hpp
index ded66eedd..00be30837 100644
--- a/obs/window-basic-settings.hpp
+++ b/obs/window-basic-settings.hpp
@@ -253,6 +253,7 @@ private slots:
void on_colorFormat_currentIndexChanged(const QString &text);
+ void on_filenameFormatting_textEdited(const QString &text);
void on_outputResolution_editTextChanged(const QString &text);
void on_baseResolution_editTextChanged(const QString &text);