diff --git a/obs/data/locale/en-US.ini b/obs/data/locale/en-US.ini index 9d083d99a..528a965d6 100644 --- a/obs/data/locale/en-US.ini +++ b/obs/data/locale/en-US.ini @@ -175,6 +175,10 @@ Basic.InteractionWindow="Interacting with '%1'" Basic.StatusBar.Reconnecting="Disconnected, reconnecting in %2 second(s) (attempt %1)" Basic.StatusBar.AttemptingReconnect="Attempting to reconnect... (attempt %1)" Basic.StatusBar.ReconnectSuccessful="Reconnection successful" +Basic.StatusBar.Delay="Delay (%1 sec)" +Basic.StatusBar.DelayStartingIn="Delay (starting in %1 sec)" +Basic.StatusBar.DelayStoppingIn="Delay (stopping in %1 sec)" +Basic.StatusBar.DelayStartingStoppingIn="Delay (stopping in %1 sec, starting in %2 sec)" # filters window Basic.Filters="Filters" @@ -225,6 +229,7 @@ Basic.Main.StartRecording="Start Recording" Basic.Main.StartStreaming="Start Streaming" Basic.Main.StopRecording="Stop Recording" Basic.Main.StopStreaming="Stop Streaming" +Basic.Main.ForceStopStreaming="Stop Streaming (discard delay)" # basic mode file menu Basic.MainMenu.File="&File" diff --git a/obs/window-basic-main-outputs.cpp b/obs/window-basic-main-outputs.cpp index c5743dbde..0f71fac1b 100644 --- a/obs/window-basic-main-outputs.cpp +++ b/obs/window-basic-main-outputs.cpp @@ -6,6 +6,33 @@ using namespace std; +static void OBSStreamStarting(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + obs_output_t *obj = (obs_output_t*)calldata_ptr(params, "output"); + + int sec = (int)obs_output_get_active_delay(obj); + if (sec == 0) + return; + + output->delayActive = true; + QMetaObject::invokeMethod(output->main, + "StreamDelayStarting", Q_ARG(int, sec)); +} + +static void OBSStreamStopping(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + obs_output_t *obj = (obs_output_t*)calldata_ptr(params, "output"); + + int sec = (int)obs_output_get_active_delay(obj); + if (sec == 0) + return; + + QMetaObject::invokeMethod(output->main, + "StreamDelayStopping", Q_ARG(int, sec)); +} + static void OBSStartStreaming(void *data, calldata_t *params) { BasicOutputHandler *output = static_cast(data); @@ -21,6 +48,7 @@ static void OBSStopStreaming(void *data, calldata_t *params) int code = (int)calldata_int(params, "code"); output->streamingActive = false; + output->delayActive = false; QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code)); } @@ -91,6 +119,7 @@ struct SimpleOutput : BasicOutputHandler { virtual bool StartStreaming(obs_service_t *service) override; virtual bool StartRecording() override; virtual void StopStreaming() override; + virtual void ForceStopStreaming() override; virtual void StopRecording() override; virtual bool StreamingActive() const override; virtual bool RecordingActive() const override; @@ -120,6 +149,11 @@ SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) "simple_aac", 0)) throw "Failed to create audio encoder (simple output)"; + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), + "starting", OBSStreamStarting, this); + streamDelayStopping.Connect(obs_output_get_signal_handler(streamOutput), + "stopping", OBSStreamStopping, this); + startStreaming.Connect(obs_output_get_signal_handler(streamOutput), "start", OBSStartStreaming, this); stopStreaming.Connect(obs_output_get_signal_handler(streamOutput), @@ -209,9 +243,18 @@ bool SimpleOutput::StartStreaming(obs_service_t *service) "RetryDelay"); int maxRetries = config_get_uint(main->Config(), "SimpleOutput", "MaxRetries"); + bool useDelay = config_get_bool(main->Config(), "Output", + "DelayEnable"); + int delaySec = config_get_int(main->Config(), "Output", + "DelaySec"); + bool preserveDelay = config_get_bool(main->Config(), "Output", + "DelayPreserve"); if (!reconnect) maxRetries = 0; + obs_output_set_delay(streamOutput, useDelay ? delaySec : 0, + preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); + obs_output_set_reconnect_settings(streamOutput, maxRetries, retryDelay); @@ -276,6 +319,11 @@ void SimpleOutput::StopStreaming() obs_output_stop(streamOutput); } +void SimpleOutput::ForceStopStreaming() +{ + obs_output_force_stop(streamOutput); +} + void SimpleOutput::StopRecording() { obs_output_stop(fileOutput); @@ -320,6 +368,7 @@ struct AdvancedOutput : BasicOutputHandler { virtual bool StartStreaming(obs_service_t *service) override; virtual bool StartRecording() override; virtual void StopStreaming() override; + virtual void ForceStopStreaming() override; virtual void StopRecording() override; virtual bool StreamingActive() const override; virtual bool RecordingActive() const override; @@ -409,6 +458,11 @@ AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) "(advanced output)"; } + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), + "starting", OBSStreamStarting, this); + streamDelayStopping.Connect(obs_output_get_signal_handler(streamOutput), + "stopping", OBSStreamStopping, this); + startStreaming.Connect(obs_output_get_signal_handler(streamOutput), "start", OBSStartStreaming, this); stopStreaming.Connect(obs_output_get_signal_handler(streamOutput), @@ -684,9 +738,18 @@ bool AdvancedOutput::StartStreaming(obs_service_t *service) bool reconnect = config_get_bool(main->Config(), "AdvOut", "Reconnect"); int retryDelay = config_get_int(main->Config(), "AdvOut", "RetryDelay"); int maxRetries = config_get_int(main->Config(), "AdvOut", "MaxRetries"); + bool useDelay = config_get_bool(main->Config(), "Output", + "DelayEnable"); + int delaySec = config_get_int(main->Config(), "Output", + "DelaySec"); + bool preserveDelay = config_get_bool(main->Config(), "Output", + "DelayPreserve"); if (!reconnect) maxRetries = 0; + obs_output_set_delay(streamOutput, useDelay ? delaySec : 0, + preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); + obs_output_set_reconnect_settings(streamOutput, maxRetries, retryDelay); @@ -763,6 +826,11 @@ void AdvancedOutput::StopStreaming() obs_output_stop(streamOutput); } +void AdvancedOutput::ForceStopStreaming() +{ + obs_output_force_stop(streamOutput); +} + void AdvancedOutput::StopRecording() { obs_output_stop(fileOutput); diff --git a/obs/window-basic-main-outputs.hpp b/obs/window-basic-main-outputs.hpp index e6e673c16..ec041c004 100644 --- a/obs/window-basic-main-outputs.hpp +++ b/obs/window-basic-main-outputs.hpp @@ -7,12 +7,15 @@ struct BasicOutputHandler { OBSOutput streamOutput; bool streamingActive = false; bool recordingActive = false; + bool delayActive = false; OBSBasic *main; OBSSignal startRecording; OBSSignal stopRecording; OBSSignal startStreaming; OBSSignal stopStreaming; + OBSSignal streamDelayStarting; + OBSSignal streamDelayStopping; inline BasicOutputHandler(OBSBasic *main_) : main(main_) {} @@ -21,6 +24,7 @@ struct BasicOutputHandler { virtual bool StartStreaming(obs_service_t *service) = 0; virtual bool StartRecording() = 0; virtual void StopStreaming() = 0; + virtual void ForceStopStreaming() = 0; virtual void StopRecording() = 0; virtual bool StreamingActive() const = 0; virtual bool RecordingActive() const = 0; @@ -29,7 +33,7 @@ struct BasicOutputHandler { inline bool Active() const { - return streamingActive || recordingActive; + return streamingActive || recordingActive || delayActive; } }; diff --git a/obs/window-basic-main.cpp b/obs/window-basic-main.cpp index 2f77cc11b..d7bc3daea 100644 --- a/obs/window-basic-main.cpp +++ b/obs/window-basic-main.cpp @@ -1020,6 +1020,15 @@ void OBSBasic::CreateHotkeys() return res; }; + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) + { + obs_data_array_t *array = + obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + obs_data_array_release(array); + }; + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1) { @@ -1057,6 +1066,21 @@ void OBSBasic::CreateHotkeys() LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + auto cb = [] (void *data, obs_hotkey_id, obs_hotkey_t*, bool pressed) + { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend( + "OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), + cb, this); + LoadHotkey(forceStreamingStopHotkey, + "OBSBasic.ForceStopStreaming"); + recordingHotkeys = obs_hotkey_pair_register_frontend( "OBSBasic.StartRecording", Str("Basic.Hotkeys.StartRecording"), @@ -1077,6 +1101,7 @@ void OBSBasic::ClearHotkeys() { obs_hotkey_pair_unregister(streamingHotkeys); obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); } OBSBasic::~OBSBasic() @@ -3051,12 +3076,12 @@ void OBSBasic::StartStreaming() { SaveProject(); - if (outputHandler->StreamingActive()) - return; + ui->streamButton->setEnabled(false); + ui->streamButton->setText(QTStr("Basic.Main.Connecting")); - if (outputHandler->StartStreaming(service)) { - ui->streamButton->setEnabled(false); - ui->streamButton->setText(QTStr("Basic.Main.Connecting")); + if (!outputHandler->StartStreaming(service)) { + ui->streamButton->setText(QTStr("Basic.Main.StartStreaming")); + ui->streamButton->setEnabled(true); } } @@ -3072,6 +3097,54 @@ void OBSBasic::StopStreaming() } } +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->ForceStopStreaming(); + + if (!outputHandler->Active()) { + ui->profileMenu->setEnabled(true); + } +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + ui->streamButton->setText(QTStr("Basic.Main.StopStreaming")); + ui->streamButton->setEnabled(true); + + if (!startStreamMenu.isNull()) + startStreamMenu->deleteLater(); + + startStreamMenu = new QMenu(); + startStreamMenu->addAction(QTStr("Basic.Main.StopStreaming"), + this, SLOT(StopStreaming())); + startStreamMenu->addAction(QTStr("Basic.Main.ForceStopStreaming"), + this, SLOT(ForceStopStreaming())); + ui->streamButton->setMenu(startStreamMenu); + + ui->statusbar->StreamDelayStarting(sec); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + ui->streamButton->setText(QTStr("Basic.Main.StartStreaming")); + ui->streamButton->setEnabled(true); + + if (!startStreamMenu.isNull()) + startStreamMenu->deleteLater(); + + startStreamMenu = new QMenu(); + startStreamMenu->addAction(QTStr("Basic.Main.StartStreaming"), + this, SLOT(StartStreaming())); + startStreamMenu->addAction(QTStr("Basic.Main.ForceStopStreaming"), + this, SLOT(ForceStopStreaming())); + ui->streamButton->setMenu(startStreamMenu); + + ui->statusbar->StreamDelayStopping(sec); +} + void OBSBasic::StreamingStart() { ui->streamButton->setText(QTStr("Basic.Main.StopStreaming")); @@ -3123,6 +3196,12 @@ void OBSBasic::StreamingStop(int code) QMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + if (!startStreamMenu.isNull()) { + ui->streamButton->setMenu(nullptr); + startStreamMenu->deleteLater(); + startStreamMenu = nullptr; + } } void OBSBasic::StartRecording() diff --git a/obs/window-basic-main.hpp b/obs/window-basic-main.hpp index 94bd45bf5..ff11e6098 100644 --- a/obs/window-basic-main.hpp +++ b/obs/window-basic-main.hpp @@ -58,6 +58,7 @@ class OBSBasic : public OBSMainWindow { Q_OBJECT friend class OBSBasicPreview; + friend class OBSBasicStatusBar; enum class MoveDir { Up, @@ -106,6 +107,8 @@ private: QPointer projectors[10]; + QPointer startStreamMenu; + void DrawBackdrop(float cx, float cy); void SetupEncoders(); @@ -187,10 +190,15 @@ private: void SaveProjectNow(); obs_hotkey_pair_id streamingHotkeys, recordingHotkeys; + obs_hotkey_id forceStreamingStopHotkey; public slots: void StartStreaming(); void StopStreaming(); + void ForceStopStreaming(); + + void StreamDelayStarting(int sec); + void StreamDelayStopping(int sec); void StreamingStart(); void StreamingStop(int errorcode); diff --git a/obs/window-basic-status-bar.cpp b/obs/window-basic-status-bar.cpp index 96ef3aae0..ebda338e9 100644 --- a/obs/window-basic-status-bar.cpp +++ b/obs/window-basic-status-bar.cpp @@ -2,9 +2,11 @@ #include "obs-app.hpp" #include "window-basic-main.hpp" #include "window-basic-status-bar.hpp" +#include "window-basic-main-outputs.hpp" OBSBasicStatusBar::OBSBasicStatusBar(QWidget *parent) : QStatusBar (parent), + delayInfo (new QLabel), droppedFrames (new QLabel), sessionTime (new QLabel), cpuUsage (new QLabel), @@ -13,11 +15,13 @@ OBSBasicStatusBar::OBSBasicStatusBar(QWidget *parent) sessionTime->setText(QString("00:00:00")); cpuUsage->setText(QString("CPU: 0.0%")); + delayInfo->setAlignment(Qt::AlignRight); droppedFrames->setAlignment(Qt::AlignRight); sessionTime->setAlignment(Qt::AlignRight); cpuUsage->setAlignment(Qt::AlignRight); kbps->setAlignment(Qt::AlignRight); + delayInfo->setIndent(20); droppedFrames->setIndent(20); sessionTime->setIndent(20); cpuUsage->setIndent(20); @@ -26,30 +30,69 @@ OBSBasicStatusBar::OBSBasicStatusBar(QWidget *parent) addPermanentWidget(droppedFrames); addPermanentWidget(sessionTime); addPermanentWidget(cpuUsage); + addPermanentWidget(delayInfo); addPermanentWidget(kbps); } -void OBSBasicStatusBar::IncRef() +void OBSBasicStatusBar::Activate() { - if (activeRefs++ == 0) { + if (!active) { refreshTimer = new QTimer(this); connect(refreshTimer, SIGNAL(timeout()), this, SLOT(UpdateStatusBar())); totalSeconds = 0; refreshTimer->start(1000); + + active = true; } } -void OBSBasicStatusBar::DecRef() +void OBSBasicStatusBar::Deactivate() { - if (--activeRefs == 0) { + OBSBasic *main = qobject_cast(parent()); + if (!main) + return; + + if (!main->outputHandler->Active()) { delete refreshTimer; sessionTime->setText(QString("00:00:00")); + delayInfo->setText(""); droppedFrames->setText(""); kbps->setText(""); + + delaySecTotal = 0; + delaySecStarting = 0; + delaySecStopping = 0; + active = false; } } +void OBSBasicStatusBar::UpdateDelayMsg() +{ + QString msg; + + if (delaySecTotal) { + if (delaySecStarting && !delaySecStopping) { + msg = QTStr("Basic.StatusBar.DelayStartingIn"); + msg = msg.arg(QString::number(delaySecStarting)); + + } else if (!delaySecStarting && delaySecStopping) { + msg = QTStr("Basic.StatusBar.DelayStoppingIn"); + msg = msg.arg(QString::number(delaySecStopping)); + + } else if (delaySecStarting && delaySecStopping) { + msg = QTStr("Basic.StatusBar.DelayStartingStoppingIn"); + msg = msg.arg(QString::number(delaySecStopping), + QString::number(delaySecStarting)); + } else { + msg = QTStr("Basic.StatusBar.Delay"); + msg = msg.arg(QString::number(delaySecTotal)); + } + } + + delayInfo->setText(msg); +} + #define BITRATE_UPDATE_SECONDS 2 void OBSBasicStatusBar::UpdateBandwidth() @@ -62,6 +105,10 @@ void OBSBasicStatusBar::UpdateBandwidth() uint64_t bytesSent = obs_output_get_total_bytes(streamOutput); uint64_t bytesSentTime = os_gettime_ns(); + + if (bytesSent == 0) + lastBytesSent = 0; + uint64_t bitsBetween = (bytesSent - lastBytesSent) * 8; double timePassed = double(bytesSentTime - lastBytesSentTime) / @@ -117,6 +164,14 @@ void OBSBasicStatusBar::UpdateSessionTime() QString msg = QTStr("Basic.StatusBar.AttemptingReconnect"); showMessage(msg.arg(QString::number(retries))); } + + if (delaySecStopping > 0 || delaySecStarting > 0) { + if (delaySecStopping > 0) + --delaySecStopping; + if (delaySecStarting > 0) + --delaySecStarting; + UpdateDelayMsg(); + } } void OBSBasicStatusBar::UpdateDroppedFrames() @@ -161,6 +216,11 @@ void OBSBasicStatusBar::Reconnect(int seconds) { retries++; reconnectTimeout = seconds; + + if (streamOutput) { + delaySecTotal = obs_output_get_active_delay(streamOutput); + UpdateDelayMsg(); + } } void OBSBasicStatusBar::ReconnectSuccess() @@ -171,6 +231,11 @@ void OBSBasicStatusBar::ReconnectSuccess() bitrateUpdateSeconds = -1; lastBytesSent = 0; lastBytesSentTime = os_gettime_ns(); + + if (streamOutput) { + delaySecTotal = obs_output_get_active_delay(streamOutput); + UpdateDelayMsg(); + } } void OBSBasicStatusBar::UpdateStatusBar() @@ -180,6 +245,25 @@ void OBSBasicStatusBar::UpdateStatusBar() UpdateDroppedFrames(); } +void OBSBasicStatusBar::StreamDelayStarting(int sec) +{ + OBSBasic *main = qobject_cast(parent()); + if (!main || !main->outputHandler) + return; + + streamOutput = main->outputHandler->streamOutput; + + delaySecTotal = delaySecStarting = sec; + UpdateDelayMsg(); + Activate(); +} + +void OBSBasicStatusBar::StreamDelayStopping(int sec) +{ + delaySecTotal = delaySecStopping = sec; + UpdateDelayMsg(); +} + void OBSBasicStatusBar::StreamStarted(obs_output_t *output) { streamOutput = output; @@ -192,7 +276,7 @@ void OBSBasicStatusBar::StreamStarted(obs_output_t *output) retries = 0; lastBytesSent = 0; lastBytesSentTime = os_gettime_ns(); - IncRef(); + Activate(); } void OBSBasicStatusBar::StreamStopped() @@ -208,18 +292,18 @@ void OBSBasicStatusBar::StreamStopped() streamOutput = nullptr; clearMessage(); - DecRef(); + Deactivate(); } } void OBSBasicStatusBar::RecordingStarted(obs_output_t *output) { recordOutput = output; - IncRef(); + Activate(); } void OBSBasicStatusBar::RecordingStopped() { recordOutput = nullptr; - DecRef(); + Deactivate(); } diff --git a/obs/window-basic-status-bar.hpp b/obs/window-basic-status-bar.hpp index 8d75d404a..af9a04bbe 100644 --- a/obs/window-basic-status-bar.hpp +++ b/obs/window-basic-status-bar.hpp @@ -12,6 +12,7 @@ class OBSBasicStatusBar : public QStatusBar { Q_OBJECT private: + QLabel *delayInfo; QLabel *droppedFrames; QLabel *sessionTime; QLabel *cpuUsage; @@ -19,24 +20,29 @@ private: obs_output_t *streamOutput = nullptr; obs_output_t *recordOutput = nullptr; + bool active = false; int retries = 0; - int activeRefs = 0; int totalSeconds = 0; int reconnectTimeout = 0; + int delaySecTotal = 0; + int delaySecStarting = 0; + int delaySecStopping = 0; + int bitrateUpdateSeconds = 0; uint64_t lastBytesSent = 0; uint64_t lastBytesSentTime = 0; QPointer refreshTimer; - void DecRef(); - void IncRef(); - obs_output_t *GetOutput(); + void Activate(); + void Deactivate(); + + void UpdateDelayMsg(); void UpdateBandwidth(); void UpdateSessionTime(); void UpdateDroppedFrames(); @@ -53,6 +59,8 @@ private slots: public: OBSBasicStatusBar(QWidget *parent); + void StreamDelayStarting(int sec); + void StreamDelayStopping(int sec); void StreamStarted(obs_output_t *output); void StreamStopped(); void RecordingStarted(obs_output_t *output);