/****************************************************************************** Copyright (C) 2023 by Lain Bailey Zachary Lund Philippe Groarke This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ #include "OBSBasic.hpp" #include #include #include #include #include void OBSBasic::on_actionShow_Recordings_triggered() { const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); const char *adv_path = strcmp(type, "Standard") ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") : adv_path; QDesktopServices::openUrl(QUrl::fromLocalFile(path)); } #define RECORDING_START "==== Recording Start ===============================================" #define RECORDING_STOP "==== Recording Stop ================================================" extern volatile bool replaybuf_active; void OBSBasic::AutoRemux(QString input, bool no_show) { auto config = Config(); bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); if (!autoRemux) return; bool isSimpleMode = false; const char *mode = config_get_string(config, "Output", "Mode"); if (!mode) { isSimpleMode = true; } else { isSimpleMode = strcmp(mode, "Simple") == 0; } if (!isSimpleMode) { const char *recType = config_get_string(config, "AdvOut", "RecType"); bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; if (ffmpegOutput) return; } if (input.isEmpty()) return; QFileInfo fi(input); QString suffix = fi.suffix(); /* do not remux if lossless */ if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { return; } QString path = fi.path(); QString output = input; output.resize(output.size() - suffix.size()); const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); const char *vCodecName = obs_encoder_get_codec(videoEncoder); const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); /* Retain original container for fMP4/fMOV */ if (strncmp(format, "fragmented", 10) == 0) { output += "remuxed." + suffix; } else if (strcmp(vCodecName, "prores") == 0) { output += "mov"; } else { output += "mp4"; } OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); if (!no_show) remux->show(); remux->AutoRemux(input, output); } void OBSBasic::StartRecording() { if (outputHandler->RecordingActive()) return; if (disableOutputsRef) return; if (!OutputPathValid()) { OutputPathInvalidMessage(); return; } if (!IsFFmpegOutputToURL() && LowDiskSpace()) { DiskSpaceMessage(); return; } OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); SaveProject(); outputHandler->StartRecording(); } void OBSBasic::RecordStopping() { emit RecordingStopping(); if (sysTrayRecord) sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); recordingStopping = true; OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); } void OBSBasic::StopRecording() { SaveProject(); if (outputHandler->RecordingActive()) outputHandler->StopRecording(recordingStopping); OnDeactivate(); } void OBSBasic::RecordingStart() { ui->statusbar->RecordingStarted(outputHandler->fileOutput); emit RecordingStarted(isRecordingPausable); if (sysTrayRecord) sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); recordingStopping = false; OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); if (!diskFullTimer->isActive()) diskFullTimer->start(1000); OnActivate(); blog(LOG_INFO, RECORDING_START); } void OBSBasic::RecordingStop(int code, QString last_error) { ui->statusbar->RecordingStopped(); emit RecordingStopped(); if (sysTrayRecord) sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); blog(LOG_INFO, RECORDING_STOP); if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { QString msg = last_error.isEmpty() ? QTStr("Output.RecordError.EncodeErrorMsg") : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { const char *errorDescription; DStr errorMessage; bool use_last_error = true; errorDescription = Str("Output.RecordError.Msg"); if (use_last_error && !last_error.isEmpty()) dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); else dstr_copy(errorMessage, errorDescription); OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); } else if (code == OBS_OUTPUT_SUCCESS) { if (outputHandler) { std::string path = outputHandler->lastRecordingPath; QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); } } OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); if (diskFullTimer->isActive()) diskFullTimer->stop(); AutoRemux(outputHandler->lastRecordingPath.c_str()); OnDeactivate(); } void OBSBasic::RecordingFileChanged(QString lastRecordingPath) { QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); ShowStatusBarMessage(str.arg(lastRecordingPath)); AutoRemux(lastRecordingPath, true); } void OBSBasic::RecordActionTriggered() { if (outputHandler->RecordingActive()) { bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); if (confirm && isVisible()) { QMessageBox::StandardButton button = OBSMessageBox::question( this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (button == QMessageBox::No) return; } StopRecording(); } else { if (!UIValidation::NoSourcesConfirmation(this)) return; StartRecording(); } } bool OBSBasic::RecordingActive() { if (!outputHandler) return false; return outputHandler->RecordingActive(); } void OBSBasic::PauseRecording() { if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || os_atomic_load_bool(&recording_paused)) return; obs_output_t *output = outputHandler->fileOutput; if (obs_output_pause(output, true)) { os_atomic_set_bool(&recording_paused, true); emit RecordingPaused(); ui->statusbar->RecordingPaused(); TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); if (trayIcon && trayIcon->isVisible()) { #ifdef __APPLE__ QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); trayIconFile.setIsMask(true); #else QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); #endif trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); } OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); if (os_atomic_load_bool(&replaybuf_active)) ShowReplayBufferPauseWarning(); } } void OBSBasic::UnpauseRecording() { if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || !os_atomic_load_bool(&recording_paused)) return; obs_output_t *output = outputHandler->fileOutput; if (obs_output_pause(output, false)) { os_atomic_set_bool(&recording_paused, false); emit RecordingUnpaused(); ui->statusbar->RecordingUnpaused(); TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); if (trayIcon && trayIcon->isVisible()) { #ifdef __APPLE__ QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); trayIconFile.setIsMask(true); #else QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); #endif trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); } OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); } } void OBSBasic::RecordPauseToggled() { if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) return; obs_output_t *output = outputHandler->fileOutput; bool enable = !obs_output_paused(output); if (enable) PauseRecording(); else UnpauseRecording(); } void OBSBasic::UpdateIsRecordingPausable() { const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); bool adv = astrcmpi(mode, "Advanced") == 0; bool shared = true; if (adv) { const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); if (astrcmpi(recType, "FFmpeg") == 0) { shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); } else { const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); shared = astrcmpi(recordEncoder, "none") == 0; } } else { const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); shared = strcmp(quality, "Stream") == 0; } isRecordingPausable = !shared; } #define MBYTE (1024ULL * 1024ULL) #define MBYTES_LEFT_STOP_REC 50ULL #define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) void OBSBasic::DiskSpaceMessage() { blog(LOG_ERROR, "Recording stopped because of low disk space"); OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); } bool OBSBasic::LowDiskSpace() { const char *path; path = GetCurrentOutputPath(); if (!path) return false; uint64_t num_bytes = os_get_free_disk_space(path); if (num_bytes < (MAX_BYTES_LEFT)) return true; else return false; } void OBSBasic::CheckDiskSpaceRemaining() { if (LowDiskSpace()) { StopRecording(); StopReplayBuffer(); DiskSpaceMessage(); } }