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