commit
b745a100a3
|
@ -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"
|
||||
|
|
|
@ -2418,6 +2418,95 @@
|
|||
<item row="6" column="1">
|
||||
<widget class="QLineEdit" name="advOutMuxCustom"/>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<widget class="QCheckBox" name="advOutSplitFile">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="layoutDirection">
|
||||
<enum>Qt::RightToLeft</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Basic.Settings.Output.EnableSplitFile</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QComboBox" name="advOutSplitFileType">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Basic.Settings.Output.SplitFile.TypeTime</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Basic.Settings.Output.SplitFile.TypeSize</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<widget class="QLabel" name="advOutSplitFileTimeLabel">
|
||||
<property name="text">
|
||||
<string>Basic.Settings.Output.SplitFile.Time</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="QSpinBox" name="advOutSplitFileTime">
|
||||
<property name="suffix">
|
||||
<string> s</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>21600</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>900</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="0">
|
||||
<widget class="QLabel" name="advOutSplitFileSizeLabel">
|
||||
<property name="text">
|
||||
<string>Basic.Settings.Output.SplitFile.Size</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="1">
|
||||
<widget class="QSpinBox" name="advOutSplitFileSize">
|
||||
<property name="suffix">
|
||||
<string> MB</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>20</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>8192</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>2048</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="1">
|
||||
<widget class="QCheckBox" name="advOutSplitFileRstTS">
|
||||
<property name="text">
|
||||
<string>Basic.Settings.Output.SplitFile.ResetTimestamps</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QStackedWidget" name="advRecTrackWidget">
|
||||
<property name="sizePolicy">
|
||||
|
@ -5784,6 +5873,11 @@
|
|||
<tabstop>advOutRecUseRescale</tabstop>
|
||||
<tabstop>advOutRecRescale</tabstop>
|
||||
<tabstop>advOutMuxCustom</tabstop>
|
||||
<tabstop>advOutSplitFile</tabstop>
|
||||
<tabstop>advOutSplitFileType</tabstop>
|
||||
<tabstop>advOutSplitFileTime</tabstop>
|
||||
<tabstop>advOutSplitFileSize</tabstop>
|
||||
<tabstop>advOutSplitFileRstTS</tabstop>
|
||||
<tabstop>advOutFFType</tabstop>
|
||||
<tabstop>advOutFFRecPath</tabstop>
|
||||
<tabstop>advOutFFPathBrowse</tabstop>
|
||||
|
@ -6307,5 +6401,21 @@
|
|||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>advOutSplitFile</sender>
|
||||
<signal>toggled(bool)</signal>
|
||||
<receiver>advOutSplitFileType</receiver>
|
||||
<slot>setEnabled(bool)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>327</x>
|
||||
<y>355</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>701</x>
|
||||
<y>355</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
|
|
|
@ -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<BasicOutputHandler *>(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<BasicOutputHandler *>(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);
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ struct BasicOutputHandler {
|
|||
OBSSignal streamDelayStarting;
|
||||
OBSSignal streamStopping;
|
||||
OBSSignal recordStopping;
|
||||
OBSSignal recordFileChanged;
|
||||
OBSSignal replayBufferStopping;
|
||||
OBSSignal replayBufferSaved;
|
||||
|
||||
|
|
|
@ -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 = []() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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; };
|
||||
|
|
|
@ -393,6 +393,7 @@ private slots:
|
|||
|
||||
void UpdateAutomaticReplayBufferCheckboxes();
|
||||
|
||||
void AdvOutSplitFileChanged();
|
||||
void AdvOutRecCheckWarnings();
|
||||
|
||||
void SimpleRecordingQualityChanged();
|
||||
|
|
|
@ -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++)
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
enum ffm_packet_type {
|
||||
FFM_PACKET_VIDEO,
|
||||
FFM_PACKET_AUDIO,
|
||||
FFM_PACKET_CHANGE_FILE,
|
||||
};
|
||||
|
||||
#define FFM_SUCCESS 0
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue