diff --git a/.github/workflows/build-project.yaml b/.github/workflows/build-project.yaml index 90b8d5b9acba33..c55818a71dbbf0 100644 --- a/.github/workflows/build-project.yaml +++ b/.github/workflows/build-project.yaml @@ -251,7 +251,51 @@ jobs: container: image: ghcr.io/flathub-infra/flatpak-github-actions:kde-6.8 options: --privileged + volumes: + - /usr/local/lib/android:/to_clean/android + - /opt/hostedtoolcache/CodeQL:/to_clean/codeql + - /usr/local/.ghcup:/to_clean/ghcup + - /opt/hostedtoolcache/Python:/to_clean/python + - /usr/share/swift:/to_clean/swift + - /usr/share/dotnet:/to_clean/dotnet steps: + - name: Prepare build space + shell: bash + run: | + : Prepare build space + + echo ::group::Available storage + df -h + echo ::endgroup:: + + echo ::group::Remove Android stuff + rm -rf /to_clean/android/* + echo ::endgroup:: + + echo ::group::Remove CodeQL stuff + rm -rf /to_clean/codeql/* + echo ::endgroup:: + + echo ::group::Remove GHCup stuff + rm -rf /to_clean/ghcup/* + echo ::endgroup:: + + echo ::group::Remove Python stuff + rm -rf /to_clean/python/* + echo ::endgroup:: + + echo ::group::Remove Swift stuff + rm -rf /to_clean/swift/* + echo ::endgroup:: + + echo ::group::Remove .NET stuff + rm -rf /to_clean/dotnet/* + echo ::endgroup:: + + echo ::group::Available storage + df -h + echo ::endgroup:: + - uses: actions/checkout@v4 with: submodules: recursive diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index ff06a4a3c388ff..3c3c5cdca58a35 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -89,10 +89,54 @@ jobs: container: image: ghcr.io/flathub-infra/flatpak-github-actions:kde-6.8 options: --privileged + volumes: + - /usr/local/lib/android:/to_clean/android + - /opt/hostedtoolcache/CodeQL:/to_clean/codeql + - /usr/local/.ghcup:/to_clean/ghcup + - /opt/hostedtoolcache/Python:/to_clean/python + - /usr/share/swift:/to_clean/swift + - /usr/share/dotnet:/to_clean/dotnet strategy: matrix: branch: ${{ fromJSON(needs.check-tag.outputs.flatpakMatrix) }} steps: + - name: Prepare build space + shell: bash + run: | + : Prepare build space + + echo ::group::Available storage + df -h + echo ::endgroup:: + + echo ::group::Remove Android stuff + rm -rf /to_clean/android/* + echo ::endgroup:: + + echo ::group::Remove CodeQL stuff + rm -rf /to_clean/codeql/* + echo ::endgroup:: + + echo ::group::Remove GHCup stuff + rm -rf /to_clean/ghcup/* + echo ::endgroup:: + + echo ::group::Remove Python stuff + rm -rf /to_clean/python/* + echo ::endgroup:: + + echo ::group::Remove Swift stuff + rm -rf /to_clean/swift/* + echo ::endgroup:: + + echo ::group::Remove .NET stuff + rm -rf /to_clean/dotnet/* + echo ::endgroup:: + + echo ::group::Available storage + df -h + echo ::endgroup:: + - uses: actions/checkout@v4 with: submodules: recursive diff --git a/docs/sphinx/reference-canvases.rst b/docs/sphinx/reference-canvases.rst index 1edf7be080869f..5639433d738758 100644 --- a/docs/sphinx/reference-canvases.rst +++ b/docs/sphinx/reference-canvases.rst @@ -1,6 +1,16 @@ Canvas API Reference (obs_canvas_t) =================================== +.. danger:: + + Canvases are still in their early stages of implementation and the API will evolve as they are gradually integrated + across OBS Studio. + + The Canvas API should be considered **unstable** and may be changed without warning. This documentation serves to aid + in their iterative development within OBS Studio. + + **If you are developing a plugin that utilizes canvases, proceed with great caution.** + Canvases are reference-counted objects that contain scenes and define how those are rendered. They provide a video object which can be used with encoders or raw outputs. diff --git a/frontend/OBSApp.cpp b/frontend/OBSApp.cpp index fb5f8dd20f634d..3e66fba22bc02b 100644 --- a/frontend/OBSApp.cpp +++ b/frontend/OBSApp.cpp @@ -42,9 +42,8 @@ #include #endif -#ifdef _WIN32 #include -#else +#ifndef _WIN32 #include #endif @@ -78,6 +77,7 @@ extern string opt_starting_profile; #ifndef _WIN32 int OBSApp::sigintFd[2]; +int OBSApp::sigtermFd[2]; #endif // GPU hint exports for AMD/NVIDIA laptops @@ -868,6 +868,8 @@ OBSApp::OBSApp(int &argc, char **argv, profiler_name_store_t *store) profilerNameStore(store), appLaunchUUID_(QUuid::createUuid()) { + installNativeEventFilter(new OBS::NativeEventFilter); + /* fix float handling */ #if defined(Q_OS_UNIX) if (!setlocale(LC_NUMERIC, "C")) @@ -879,9 +881,14 @@ OBSApp::OBSApp(int &argc, char **argv, profiler_name_store_t *store) socketpair(AF_UNIX, SOCK_STREAM, 0, sigintFd); snInt = new QSocketNotifier(sigintFd[1], QSocketNotifier::Read, this); connect(snInt, &QSocketNotifier::activated, this, &OBSApp::ProcessSigInt); -#else - connect(qApp, &QGuiApplication::commitDataRequest, this, &OBSApp::commitData); + + /* Handle SIGTERM */ + socketpair(AF_UNIX, SOCK_STREAM, 0, sigtermFd); + snTerm = new QSocketNotifier(sigtermFd[1], QSocketNotifier::Read, this); + connect(snTerm, &QSocketNotifier::activated, this, &OBSApp::ProcessSigTerm); #endif + connect(qApp, &QGuiApplication::commitDataRequest, this, &OBSApp::commitData, Qt::DirectConnection); + if (multi) { crashHandler_ = std::make_unique(); } else { @@ -1229,10 +1236,24 @@ bool OBSApp::OBSInit() mainWindow = new OBSBasic(); mainWindow->setAttribute(Qt::WA_DeleteOnClose, true); - connect(mainWindow, &OBSBasic::destroyed, this, &OBSApp::quit); + + connect(QApplication::instance(), &QApplication::aboutToQuit, this, [this]() { + /* Ensure OBSMainWindow gets closed */ + if (mainWindow) { + mainWindow->close(); + delete mainWindow; + } + + if (libobs_initialized) { + applicationShutdown(); + } + }); mainWindow->OBSInit(); + connect(OBSBasic::Get(), &OBSBasic::mainWindowClosed, crashHandler_.get(), + &OBS::CrashHandler::applicationShutdownHandler); + connect(this, &QGuiApplication::applicationStateChanged, [this](Qt::ApplicationState state) { ResetHotkeyState(state == Qt::ApplicationActive); }); ResetHotkeyState(applicationState() == Qt::ApplicationActive); @@ -1748,6 +1769,14 @@ void OBSApp::SigIntSignalHandler(int s) char a = 1; send(sigintFd[0], &a, sizeof(a), 0); } + +void OBSApp::SigTermSignalHandler(int s) +{ + UNUSED_PARAMETER(s); + + char a = 1; + send(sigtermFd[0], &a, sizeof(a), 0); +} #endif void OBSApp::ProcessSigInt(void) @@ -1759,20 +1788,39 @@ void OBSApp::ProcessSigInt(void) recv(sigintFd[1], &tmp, sizeof(tmp), 0); OBSBasic *main = OBSBasic::Get(); - if (main) + if (main) { + main->saveAll(); main->close(); + } +#endif +} + +void OBSApp::ProcessSigTerm(void) +{ +#ifndef _WIN32 + char tmp; + recv(sigtermFd[1], &tmp, sizeof(tmp), 0); + + OBSBasic *main = OBSBasic::Get(); + if (main) { + main->saveAll(); + } + + quit(); #endif } -#ifdef _WIN32 void OBSApp::commitData(QSessionManager &manager) { - if (auto main = App()->GetMainWindow()) { - QMetaObject::invokeMethod(main, "close", Qt::QueuedConnection); - manager.cancel(); + OBSBasic *main = OBSBasic::Get(); + if (main) { + main->saveAll(); + + if (manager.allowsInteraction() && main->shouldPromptForClose()) { + manager.cancel(); + } } } -#endif void OBSApp::applicationShutdown() noexcept { @@ -1784,6 +1832,10 @@ void OBSApp::applicationShutdown() noexcept delete snInt; close(sigintFd[0]); close(sigintFd[1]); + + delete snTerm; + close(sigtermFd[0]); + close(sigtermFd[1]); #endif #ifdef __APPLE__ diff --git a/frontend/OBSApp.hpp b/frontend/OBSApp.hpp index b2524a105d5473..2cb010b1962e4c 100644 --- a/frontend/OBSApp.hpp +++ b/frontend/OBSApp.hpp @@ -18,6 +18,7 @@ #pragma once #include +#include #include #include @@ -25,6 +26,7 @@ #include #include +#include #include #include #include @@ -61,6 +63,8 @@ struct UpdateBranch { class OBSApp : public QApplication { Q_OBJECT + friend class OBS::NativeEventFilter; + private: QUuid appLaunchUUID_; std::unique_ptr crashHandler_; @@ -117,12 +121,13 @@ class OBSApp : public QApplication { #ifndef _WIN32 static int sigintFd[2]; QSocketNotifier *snInt = nullptr; -#else -private slots: - void commitData(QSessionManager &manager); + + static int sigtermFd[2]; + QSocketNotifier *snTerm = nullptr; #endif private slots: + void commitData(QSessionManager &manager); void addLogLine(int logLevel, const QString &message); void themeFileChanged(const QString &); void applicationShutdown() noexcept; @@ -212,6 +217,7 @@ private slots: inline void PopUITranslation() { translatorHooks.pop_front(); } #ifndef _WIN32 static void SigIntSignalHandler(int); + static void SigTermSignalHandler(int); #endif void loadAppModules(struct obs_module_failure_info &mfi); @@ -222,6 +228,7 @@ private slots: public slots: void Exec(VoidFunc func); void ProcessSigInt(); + void ProcessSigTerm(); signals: void logLineAdded(int logLevel, const QString &message); diff --git a/frontend/cmake/feature-idian-playground.cmake b/frontend/cmake/feature-idian-playground.cmake index 8c9daf0485102b..61dfe5ddcb81d9 100644 --- a/frontend/cmake/feature-idian-playground.cmake +++ b/frontend/cmake/feature-idian-playground.cmake @@ -3,7 +3,7 @@ option(ENABLE_IDIAN_PLAYGROUND "Enable building custom idian widget demo window" if(ENABLE_IDIAN_PLAYGROUND) target_sources( obs-studio - PRIVATE forms/OBSIdianPlayground.ui dialogs/OBSIdianPlayground.hpp dialogs/OBSIdianPlayground.cpp + PRIVATE dialogs/OBSIdianPlayground.hpp dialogs/OBSIdianPlayground.cpp forms/OBSIdianPlayground.ui ) target_enable_feature(obs-studio "Idian Playground" ENABLE_IDIAN_PLAYGROUND) else() diff --git a/frontend/cmake/feature-macos-update.cmake b/frontend/cmake/feature-macos-update.cmake index da654babf64297..994290a772ac0d 100644 --- a/frontend/cmake/feature-macos-update.cmake +++ b/frontend/cmake/feature-macos-update.cmake @@ -9,16 +9,16 @@ endif() target_sources( obs-studio PRIVATE + utility/WhatsNewBrowserInitThread.cpp + utility/WhatsNewBrowserInitThread.hpp + utility/WhatsNewInfoThread.cpp + utility/WhatsNewInfoThread.hpp utility/crypto-helpers-mac.mm utility/crypto-helpers.hpp utility/models/branches.hpp utility/models/whatsnew.hpp utility/update-helpers.cpp utility/update-helpers.hpp - utility/WhatsNewBrowserInitThread.cpp - utility/WhatsNewBrowserInitThread.hpp - utility/WhatsNewInfoThread.cpp - utility/WhatsNewInfoThread.hpp ) target_link_libraries( diff --git a/frontend/cmake/feature-whatsnew.cmake b/frontend/cmake/feature-whatsnew.cmake index c7afb49a96b0a2..fe5fb4437ec6f8 100644 --- a/frontend/cmake/feature-whatsnew.cmake +++ b/frontend/cmake/feature-whatsnew.cmake @@ -20,15 +20,15 @@ if(ENABLE_WHATSNEW AND TARGET OBS::browser-panels) target_sources( obs-studio PRIVATE + utility/WhatsNewBrowserInitThread.cpp + utility/WhatsNewBrowserInitThread.hpp + utility/WhatsNewInfoThread.cpp + utility/WhatsNewInfoThread.hpp utility/crypto-helpers-mbedtls.cpp utility/crypto-helpers.hpp utility/models/whatsnew.hpp utility/update-helpers.cpp utility/update-helpers.hpp - utility/WhatsNewBrowserInitThread.cpp - utility/WhatsNewBrowserInitThread.hpp - utility/WhatsNewInfoThread.cpp - utility/WhatsNewInfoThread.hpp ) endif() diff --git a/frontend/cmake/os-freebsd.cmake b/frontend/cmake/os-freebsd.cmake index f3423ff4925ec5..128d681d86b49c 100644 --- a/frontend/cmake/os-freebsd.cmake +++ b/frontend/cmake/os-freebsd.cmake @@ -1,6 +1,10 @@ target_sources( obs-studio - PRIVATE utility/platform-x11.cpp utility/system-info-posix.cpp utility/CrashHandler_FreeBSD.cpp + PRIVATE + utility/CrashHandler_FreeBSD.cpp + utility/NativeEventFilter.cpp + utility/platform-x11.cpp + utility/system-info-posix.cpp ) target_compile_definitions(obs-studio PRIVATE OBS_INSTALL_PREFIX="${OBS_INSTALL_PREFIX}") target_link_libraries(obs-studio PRIVATE Qt::GuiPrivate Qt::DBus procstat) diff --git a/frontend/cmake/os-linux.cmake b/frontend/cmake/os-linux.cmake index da14f536bb7ef4..332002edeebf79 100644 --- a/frontend/cmake/os-linux.cmake +++ b/frontend/cmake/os-linux.cmake @@ -1,4 +1,11 @@ -target_sources(obs-studio PRIVATE utility/platform-x11.cpp utility/system-info-posix.cpp utility/CrashHandler_Linux.cpp) +target_sources( + obs-studio + PRIVATE + utility/CrashHandler_Linux.cpp + utility/NativeEventFilter.cpp + utility/platform-x11.cpp + utility/system-info-posix.cpp +) target_compile_definitions( obs-studio PRIVATE OBS_INSTALL_PREFIX="${OBS_INSTALL_PREFIX}" $<$:ENABLE_PORTABLE_CONFIG> diff --git a/frontend/cmake/os-macos.cmake b/frontend/cmake/os-macos.cmake index e738175a808b8f..f6a46a59f36aa5 100644 --- a/frontend/cmake/os-macos.cmake +++ b/frontend/cmake/os-macos.cmake @@ -7,6 +7,7 @@ target_sources( dialogs/OBSPermissions.hpp forms/OBSPermissions.ui utility/CrashHandler_MacOS.mm + utility/NativeEventFilter.cpp utility/platform-osx.mm utility/system-info-macos.mm ) diff --git a/frontend/cmake/os-windows.cmake b/frontend/cmake/os-windows.cmake index 98764b8b3a79d3..e8c6e0ca868287 100644 --- a/frontend/cmake/os-windows.cmake +++ b/frontend/cmake/os-windows.cmake @@ -25,6 +25,11 @@ target_sources( utility/AutoUpdateThread.cpp utility/AutoUpdateThread.hpp utility/CrashHandler_Windows.cpp + utility/NativeEventFilter_Windows.cpp + utility/WhatsNewBrowserInitThread.cpp + utility/WhatsNewBrowserInitThread.hpp + utility/WhatsNewInfoThread.cpp + utility/WhatsNewInfoThread.hpp utility/crypto-helpers-mbedtls.cpp utility/crypto-helpers.hpp utility/models/branches.hpp @@ -33,10 +38,6 @@ target_sources( utility/system-info-windows.cpp utility/update-helpers.cpp utility/update-helpers.hpp - utility/WhatsNewBrowserInitThread.cpp - utility/WhatsNewBrowserInitThread.hpp - utility/WhatsNewInfoThread.cpp - utility/WhatsNewInfoThread.hpp utility/win-dll-blocklist.c ) diff --git a/frontend/cmake/ui-models.cmake b/frontend/cmake/ui-models.cmake index a5f948740658a5..a03011a19fd68d 100644 --- a/frontend/cmake/ui-models.cmake +++ b/frontend/cmake/ui-models.cmake @@ -1 +1 @@ -target_sources(obs-studio PRIVATE models/SceneCollection.cpp models/SceneCollection.hpp models/Rect.cpp models/Rect.hpp) +target_sources(obs-studio PRIVATE models/Rect.cpp models/Rect.hpp models/SceneCollection.cpp models/SceneCollection.hpp) diff --git a/frontend/cmake/ui-qt.cmake b/frontend/cmake/ui-qt.cmake index c013dbfa273de0..4995cb5fa2e321 100644 --- a/frontend/cmake/ui-qt.cmake +++ b/frontend/cmake/ui-qt.cmake @@ -46,8 +46,8 @@ target_sources( forms/OBSMissingFiles.ui forms/OBSRemux.ui forms/StatusBarWidget.ui - forms/obs.qrc forms/PluginManagerWindow.ui + forms/obs.qrc forms/source-toolbar/browser-source-toolbar.ui forms/source-toolbar/color-source-toolbar.ui forms/source-toolbar/device-select-toolbar.ui diff --git a/frontend/cmake/ui-utility.cmake b/frontend/cmake/ui-utility.cmake index 0e1193c4110be4..c9d4c39a6150c4 100644 --- a/frontend/cmake/ui-utility.cmake +++ b/frontend/cmake/ui-utility.cmake @@ -3,14 +3,11 @@ target_sources( PRIVATE utility/AdvancedOutput.cpp utility/AdvancedOutput.hpp - utility/audio-encoders.cpp - utility/audio-encoders.hpp utility/BaseLexer.hpp utility/BasicOutputHandler.cpp utility/BasicOutputHandler.hpp utility/CrashHandler.cpp utility/CrashHandler.hpp - utility/display-helpers.hpp utility/FFmpegCodec.cpp utility/FFmpegCodec.hpp utility/FFmpegFormat.cpp @@ -22,19 +19,15 @@ target_sources( utility/GoLiveAPI_Network.hpp utility/GoLiveAPI_PostData.cpp utility/GoLiveAPI_PostData.hpp - utility/item-widget-helpers.cpp - utility/item-widget-helpers.hpp utility/MissingFilesModel.cpp utility/MissingFilesModel.hpp utility/MissingFilesPathItemDelegate.cpp utility/MissingFilesPathItemDelegate.hpp - utility/models/multitrack-video.hpp utility/MultitrackVideoError.cpp utility/MultitrackVideoError.hpp utility/MultitrackVideoOutput.cpp utility/MultitrackVideoOutput.hpp - utility/obf.c - utility/obf.h + utility/NativeEventFilter.hpp utility/OBSCanvas.cpp utility/OBSCanvas.hpp utility/OBSEventFilter.hpp @@ -44,7 +37,6 @@ target_sources( utility/OBSThemeVariable.hpp utility/OBSTranslator.cpp utility/OBSTranslator.hpp - utility/platform.hpp utility/QuickTransition.cpp utility/QuickTransition.hpp utility/RemoteTextThread.cpp @@ -64,10 +56,19 @@ target_sources( utility/SimpleOutput.hpp utility/StartMultiTrackVideoStreamingGuard.hpp utility/SurfaceEventFilter.hpp - utility/system-info.hpp - utility/undo_stack.cpp - utility/undo_stack.hpp utility/VCamConfig.hpp utility/VolumeMeterTimer.cpp utility/VolumeMeterTimer.hpp + utility/audio-encoders.cpp + utility/audio-encoders.hpp + utility/display-helpers.hpp + utility/item-widget-helpers.cpp + utility/item-widget-helpers.hpp + utility/models/multitrack-video.hpp + utility/obf.c + utility/obf.h + utility/platform.hpp + utility/system-info.hpp + utility/undo_stack.cpp + utility/undo_stack.hpp ) diff --git a/frontend/data/locale/en-US.ini b/frontend/data/locale/en-US.ini index 908af15890c577..e1a4efa32068d0 100644 --- a/frontend/data/locale/en-US.ini +++ b/frontend/data/locale/en-US.ini @@ -397,8 +397,8 @@ ConfirmBWTest.Title="Start Bandwidth Test?" ConfirmBWTest.Text="You have OBS configured in bandwidth test mode. This mode allows for network testing without your channel going live. Once you are done testing, you will need to disable it in order for viewers to be able to see your stream.\n\nDo you want to continue?" # confirm exit dialog box -ConfirmExit.Title="Exit OBS?" -ConfirmExit.Text="OBS is currently active. All streams/recordings will be shut down. Are you sure you wish to exit?" +ConfirmExit.Title="Active Outputs" +ConfirmExit.Text="OBS is still currently active. All streams/recordings will be shut down." # confirm delete dialog box ConfirmRemove.Title="Confirm Remove" @@ -1014,6 +1014,9 @@ Basic.Settings.Stream.MultitrackVideoConfigOverride="Config Override (JSON)" Basic.Settings.Stream.MultitrackVideoConfigOverrideEnable="Enable Config Override" Basic.Settings.Stream.MultitrackVideoLabel="Multitrack Video" Basic.Settings.Stream.MultitrackVideoExtraCanvas="Additional Canvas" +Basic.Settings.Stream.WHIPSimulcastLabel="Simulcast" +Basic.Settings.Stream.WHIPSimulcastInfo="Simulcast allows you to encode and send multiple video qualities. Learn More" +Basic.Settings.Stream.WHIPSimulcastTotalLayers="Total Layers" Basic.Settings.Stream.AdvancedOptions="Advanced Options" # basic mode 'output' settings diff --git a/frontend/docks/OBSDock.cpp b/frontend/docks/OBSDock.cpp index c0271b61fb00b3..aaf17b8585b0c3 100644 --- a/frontend/docks/OBSDock.cpp +++ b/frontend/docks/OBSDock.cpp @@ -28,7 +28,7 @@ void OBSDock::closeEvent(QCloseEvent *event) }; bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutClosingDocks"); - if (!OBSBasic::Get()->Closing() && !warned) { + if (!OBSBasic::Get()->isClosing() && !warned) { QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); } diff --git a/frontend/forms/OBSBasicSettings.ui b/frontend/forms/OBSBasicSettings.ui index 1d59d537b7dde5..d3d0a0ef9e0a45 100644 --- a/frontend/forms/OBSBasicSettings.ui +++ b/frontend/forms/OBSBasicSettings.ui @@ -2082,6 +2082,100 @@ + + + + Basic.Settings.Stream.WHIPSimulcastLabel + + + + 9 + + + 2 + + + 9 + + + 9 + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 170 + 10 + + + + + + + + Basic.Settings.Stream.WHIPSimulcastInfo + + + Qt::RichText + + + true + + + true + + + + + + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + Basic.Settings.Stream.WHIPSimulcastTotalLayers + + + + 170 + 0 + + + + + + + + + + 1 + + + 4 + + + 1 + + + + + + + + + + diff --git a/frontend/obs-main.cpp b/frontend/obs-main.cpp index 19ccacd686d803..6527ad627e51c5 100644 --- a/frontend/obs-main.cpp +++ b/frontend/obs-main.cpp @@ -858,13 +858,22 @@ int main(int argc, char *argv[]) #ifndef _WIN32 signal(SIGPIPE, SIG_IGN); - struct sigaction sig_handler; + struct sigaction sigint_handler; - sig_handler.sa_handler = OBSApp::SigIntSignalHandler; - sigemptyset(&sig_handler.sa_mask); - sig_handler.sa_flags = 0; + sigint_handler.sa_handler = OBSApp::SigIntSignalHandler; + sigemptyset(&sigint_handler.sa_mask); + sigint_handler.sa_flags = 0; - sigaction(SIGINT, &sig_handler, NULL); + sigaction(SIGINT, &sigint_handler, NULL); + + struct sigaction sigterm_handler; + + sigterm_handler.sa_handler = OBSApp::SigTermSignalHandler; + sigemptyset(&sigterm_handler.sa_mask); + sigterm_handler.sa_flags = 0; + + sigaction(SIGTERM, &sigterm_handler, NULL); + sigaction(SIGHUP, &sigterm_handler, NULL); /* Block SIGPIPE in all threads, this can happen if a thread calls write on a closed pipe. */ @@ -890,6 +899,14 @@ int main(int argc, char *argv[]) load_debug_privilege(); base_set_crash_handler(main_crash_handler, nullptr); + /* Shutdown priority value is a range from 0 - 4FF with higher values getting first priority. + * 000 - 0FF and 400 - 4FF are reserved system ranges. + * Processes start at shutdown level 0x280 by default. + * We set the main OBS application to a higher priority to ensure it tries to close before + * any subprocesses such as CEF. + */ + SetProcessShutdownParameters(0x300, SHUTDOWN_NORETRY); + const HMODULE hRtwq = LoadLibrary(L"RTWorkQ.dll"); if (hRtwq) { typedef HRESULT(STDAPICALLTYPE * PFN_RtwqStartup)(); diff --git a/frontend/settings/OBSBasicSettings.cpp b/frontend/settings/OBSBasicSettings.cpp index 6e47f93ac924df..eb323b65246076 100644 --- a/frontend/settings/OBSBasicSettings.cpp +++ b/frontend/settings/OBSBasicSettings.cpp @@ -385,6 +385,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->authUsername, EDIT_CHANGED, STREAM1_CHANGED); HookWidget(ui->authPw, EDIT_CHANGED, STREAM1_CHANGED); HookWidget(ui->ignoreRecommended, CHECK_CHANGED, STREAM1_CHANGED); + HookWidget(ui->whipSimulcastTotalLayers, SCROLL_CHANGED, STREAM1_CHANGED); HookWidget(ui->enableMultitrackVideo, CHECK_CHANGED, STREAM1_CHANGED); HookWidget(ui->multitrackVideoMaximumAggregateBitrateAuto, CHECK_CHANGED, STREAM1_CHANGED); HookWidget(ui->multitrackVideoMaximumAggregateBitrate, SCROLL_CHANGED, STREAM1_CHANGED); diff --git a/frontend/settings/OBSBasicSettings_Stream.cpp b/frontend/settings/OBSBasicSettings_Stream.cpp index ce74a1cb1babf4..5179f44d818360 100644 --- a/frontend/settings/OBSBasicSettings_Stream.cpp +++ b/frontend/settings/OBSBasicSettings_Stream.cpp @@ -95,6 +95,7 @@ void OBSBasicSettings::InitStreamPage() void OBSBasicSettings::LoadStream1Settings() { bool ignoreRecommended = config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + int whipSimulcastTotalLayers = config_get_int(main->Config(), "Stream1", "WHIPSimulcastTotalLayers"); obs_service_t *service_obj = main->GetService(); const char *type = obs_service_get_type(service_obj); @@ -209,10 +210,13 @@ void OBSBasicSettings::LoadStream1Settings() if (use_custom_server) ui->serviceCustomServer->setText(server); - if (is_whip) + if (is_whip) { ui->key->setText(bearer_token); - else + ui->whipSimulcastGroupBox->show(); + } else { ui->key->setText(key); + ui->whipSimulcastGroupBox->hide(); + } ServiceChanged(true); @@ -226,6 +230,7 @@ void OBSBasicSettings::LoadStream1Settings() ui->streamPage->setEnabled(!streamActive); ui->ignoreRecommended->setChecked(ignoreRecommended); + ui->whipSimulcastTotalLayers->setValue(whipSimulcastTotalLayers); loading = false; @@ -327,6 +332,9 @@ void OBSBasicSettings::SaveStream1Settings() SaveCheckBox(ui->ignoreRecommended, "Stream1", "IgnoreRecommended"); + auto oldWHIPSimulcastTotalLayers = config_get_int(main->Config(), "Stream1", "WHIPSimulcastTotalLayers"); + SaveSpinBox(ui->whipSimulcastTotalLayers, "Stream1", "WHIPSimulcastTotalLayers"); + auto oldMultitrackVideoSetting = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo"); if (!IsCustomService()) { @@ -355,7 +363,8 @@ void OBSBasicSettings::SaveStream1Settings() SaveText(ui->multitrackVideoConfigOverride, "Stream1", "MultitrackVideoConfigOverride"); SaveComboData(ui->multitrackVideoAdditionalCanvas, "Stream1", "MultitrackExtraCanvas"); - if (oldMultitrackVideoSetting != ui->enableMultitrackVideo->isChecked()) + if (oldMultitrackVideoSetting != ui->enableMultitrackVideo->isChecked() || + oldWHIPSimulcastTotalLayers != ui->whipSimulcastTotalLayers->value()) main->ResetOutputs(); SwapMultiTrack(QT_TO_UTF8(protocol)); @@ -588,6 +597,12 @@ void OBSBasicSettings::on_service_currentIndexChanged(int idx) } else { SwapMultiTrack(QT_TO_UTF8(protocol)); } + + if (IsWHIP()) { + ui->whipSimulcastGroupBox->show(); + } else { + ui->whipSimulcastGroupBox->hide(); + } } void OBSBasicSettings::on_customServer_textChanged(const QString &) diff --git a/frontend/utility/AdvancedOutput.cpp b/frontend/utility/AdvancedOutput.cpp index 996e68b7825501..ebadb5b934a0af 100644 --- a/frontend/utility/AdvancedOutput.cpp +++ b/frontend/utility/AdvancedOutput.cpp @@ -132,6 +132,12 @@ AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) throw "Failed to create streaming video encoder " "(advanced output)"; obs_encoder_release(videoStreaming); + if (whipSimulcastEncoders != nullptr) { + whipSimulcastEncoders->Create(streamEncoder, config_get_int(main->Config(), "AdvOut", "RescaleFilter"), + config_get_int(main->Config(), "Stream1", "WHIPSimulcastTotalLayers"), + video_output_get_width(obs_get_video()), + video_output_get_height(obs_get_video())); + } const char *rate_control = obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control"); @@ -247,6 +253,9 @@ void AdvancedOutput::UpdateStreamSettings() } obs_encoder_update(videoStreaming, settings); + if (whipSimulcastEncoders != nullptr) { + whipSimulcastEncoders->Update(settings, obs_data_get_int(settings, "bitrate")); + } } inline void AdvancedOutput::UpdateRecordingSettings() @@ -649,6 +658,9 @@ std::shared_future AdvancedOutput::SetupStreaming(obs_service_t *service, } obs_output_set_video_encoder(streamOutput, videoStreaming); + if (whipSimulcastEncoders != nullptr) { + whipSimulcastEncoders->SetStreamOutput(streamOutput); + } obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); if (!is_multitrack_output) { diff --git a/frontend/utility/BasicOutputHandler.cpp b/frontend/utility/BasicOutputHandler.cpp index f670fa545b4c91..a192d579185fb4 100644 --- a/frontend/utility/BasicOutputHandler.cpp +++ b/frontend/utility/BasicOutputHandler.cpp @@ -236,6 +236,9 @@ BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) if (multitrack_enabled) multitrackVideo = make_unique(); + + if (config_get_int(main->Config(), "Stream1", "WHIPSimulcastTotalLayers") > 1) + whipSimulcastEncoders = make_unique(); } extern void log_vcam_changed(const VCamConfig &config, bool starting); diff --git a/frontend/utility/BasicOutputHandler.hpp b/frontend/utility/BasicOutputHandler.hpp index 4d1a8661ed8a16..29300792a3afb8 100644 --- a/frontend/utility/BasicOutputHandler.hpp +++ b/frontend/utility/BasicOutputHandler.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -42,6 +43,8 @@ struct BasicOutputHandler { obs_scene_t *vCamSourceScene = nullptr; obs_sceneitem_t *vCamSourceSceneItem = nullptr; + std::unique_ptr whipSimulcastEncoders; + std::string outputType; std::string lastError; diff --git a/frontend/utility/NativeEventFilter.cpp b/frontend/utility/NativeEventFilter.cpp new file mode 100644 index 00000000000000..06283d88b214be --- /dev/null +++ b/frontend/utility/NativeEventFilter.cpp @@ -0,0 +1,27 @@ +/****************************************************************************** + Copyright (C) 2025 by Taylor Giampaolo + + 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 "NativeEventFilter.hpp" + +namespace OBS { + +bool NativeEventFilter::nativeEventFilter(const QByteArray &, void *, qintptr *) +{ + // Stub file for operating systems that do not need nativeEventFilter + return false; +} +} // namespace OBS diff --git a/frontend/utility/NativeEventFilter.hpp b/frontend/utility/NativeEventFilter.hpp new file mode 100644 index 00000000000000..1802d8cb82de7f --- /dev/null +++ b/frontend/utility/NativeEventFilter.hpp @@ -0,0 +1,29 @@ +/****************************************************************************** + Copyright (C) 2025 by Taylor Giampaolo + + 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 . +******************************************************************************/ + +#pragma once + +#include + +namespace OBS { + +class NativeEventFilter : public QAbstractNativeEventFilter { + +public: + bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result); +}; +} // namespace OBS diff --git a/frontend/utility/NativeEventFilter_Windows.cpp b/frontend/utility/NativeEventFilter_Windows.cpp new file mode 100644 index 00000000000000..f52449f3b223af --- /dev/null +++ b/frontend/utility/NativeEventFilter_Windows.cpp @@ -0,0 +1,66 @@ +/****************************************************************************** + Copyright (C) 2025 by Taylor Giampaolo + + 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 "NativeEventFilter.hpp" + +#include + +#include +#define WIN32_LEAN_AND_MEAN +#include + +namespace OBS { + +bool NativeEventFilter::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) +{ + if (eventType == "windows_generic_MSG") { + MSG *msg = static_cast(message); + + OBSBasic *main = OBSBasic::Get(); + if (!main) { + return false; + } + + switch (msg->message) { + case WM_QUERYENDSESSION: + main->saveAll(); + if (msg->lParam == ENDSESSION_CRITICAL) { + break; + } + + if (main->shouldPromptForClose()) { + if (result) { + *result = FALSE; + } + QTimer::singleShot(1, main, &OBSBasic::close); + return true; + } + + return false; + case WM_ENDSESSION: + if (msg->wParam == TRUE) { + // Session is ending, start closing the main window now with no checks or prompts. + main->closeWindow(); + } + + return true; + } + } + + return false; +} +} // namespace OBS diff --git a/frontend/utility/SimpleOutput.cpp b/frontend/utility/SimpleOutput.cpp index d594b9987f4ba3..4ad377c8fe73ba 100644 --- a/frontend/utility/SimpleOutput.cpp +++ b/frontend/utility/SimpleOutput.cpp @@ -75,6 +75,13 @@ void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId) if (!videoStreaming) throw "Failed to create video streaming encoder (simple output)"; obs_encoder_release(videoStreaming); + + if (whipSimulcastEncoders != nullptr) { + whipSimulcastEncoders->Create(encoderId, config_get_int(main->Config(), "AdvOut", "RescaleFilter"), + config_get_int(main->Config(), "Stream1", "WHIPSimulcastTotalLayers"), + video_output_get_width(obs_get_video()), + video_output_get_height(obs_get_video())); + } } /* mistakes have been made to lead us to this. */ @@ -351,11 +358,18 @@ void SimpleOutput::Update() break; default: obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + if (whipSimulcastEncoders != nullptr) { + whipSimulcastEncoders->SetVideoFormat(VIDEO_FORMAT_NV12); + } } obs_encoder_update(videoStreaming, videoSettings); obs_encoder_update(audioStreaming, audioSettings); obs_encoder_update(audioArchive, audioSettings); + + if (whipSimulcastEncoders != nullptr) { + whipSimulcastEncoders->Update(videoSettings, videoBitrate); + } } void SimpleOutput::UpdateRecordingAudioSettings() @@ -630,6 +644,9 @@ std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, Se } obs_output_set_video_encoder(streamOutput, videoStreaming); + if (whipSimulcastEncoders != nullptr) { + whipSimulcastEncoders->SetStreamOutput(streamOutput); + } obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); obs_output_set_service(streamOutput, service); return true; diff --git a/frontend/utility/WHIPSimulcastEncoders.hpp b/frontend/utility/WHIPSimulcastEncoders.hpp new file mode 100644 index 00000000000000..fc369eb10e8169 --- /dev/null +++ b/frontend/utility/WHIPSimulcastEncoders.hpp @@ -0,0 +1,84 @@ +/****************************************************************************** + Copyright (C) 2025 by Sean DuBois + + 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 . +******************************************************************************/ +#pragma once + +struct WHIPSimulcastEncoders { +public: + void Create(const char *encoderId, int rescaleFilter, int whipSimulcastTotalLayers, uint32_t outputWidth, + uint32_t outputHeight) + { + if (rescaleFilter == OBS_SCALE_DISABLE) { + rescaleFilter = OBS_SCALE_BICUBIC; + } + + if (whipSimulcastTotalLayers <= 1) { + return; + } + + auto widthStep = outputWidth / whipSimulcastTotalLayers; + auto heightStep = outputHeight / whipSimulcastTotalLayers; + std::string encoder_name = "whip_simulcast_0"; + + for (auto i = whipSimulcastTotalLayers - 1; i > 0; i--) { + uint32_t width = widthStep * i; + width -= width % 2; + + uint32_t height = heightStep * i; + height -= height % 2; + + encoder_name[encoder_name.size() - 1] = std::to_string(i).at(0); + auto whip_simulcast_encoder = + obs_video_encoder_create(encoderId, encoder_name.c_str(), nullptr, nullptr); + + if (whip_simulcast_encoder) { + obs_encoder_set_video(whip_simulcast_encoder, obs_get_video()); + obs_encoder_set_scaled_size(whip_simulcast_encoder, width, height); + obs_encoder_set_gpu_scale_type(whip_simulcast_encoder, (obs_scale_type)rescaleFilter); + whipSimulcastEncoders.push_back(whip_simulcast_encoder); + obs_encoder_release(whip_simulcast_encoder); + } else { + blog(LOG_WARNING, + "Failed to create video streaming WHIP Simulcast encoders (BasicOutputHandler)"); + } + } + } + + void Update(obs_data_t *videoSettings, int videoBitrate) + { + auto bitrateStep = videoBitrate / static_cast(whipSimulcastEncoders.size() + 1); + for (auto &whipSimulcastEncoder : whipSimulcastEncoders) { + videoBitrate -= bitrateStep; + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + obs_encoder_update(whipSimulcastEncoder, videoSettings); + } + } + + void SetVideoFormat(enum video_format format) + { + for (auto enc : whipSimulcastEncoders) + obs_encoder_set_preferred_video_format(enc, format); + } + + void SetStreamOutput(obs_output_t *streamOutput) + { + for (size_t i = 0; i < whipSimulcastEncoders.size(); i++) + obs_output_set_video_encoder2(streamOutput, whipSimulcastEncoders[i], i + 1); + } + +private: + std::vector whipSimulcastEncoders; +}; diff --git a/frontend/widgets/OBSBasic.cpp b/frontend/widgets/OBSBasic.cpp index b0698b29b54755..4e25e91e6c7712 100644 --- a/frontend/widgets/OBSBasic.cpp +++ b/frontend/widgets/OBSBasic.cpp @@ -558,6 +558,7 @@ OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + connect(App(), &OBSApp::aboutToQuit, this, &OBSBasic::closeWindow); QActionGroup *actionGroup = new QActionGroup(this); actionGroup->addAction(ui->actionSceneListMode); @@ -1372,8 +1373,8 @@ void OBSBasic::OnFirstLoad() OBSBasic::~OBSBasic() { - if (!handledShutdown) { - applicationShutdown(); + if (!isClosing()) { + closeWindow(); } } @@ -1457,19 +1458,6 @@ void OBSBasic::applicationShutdown() noexcept * expect or want it to. */ QApplication::sendPostedEvents(nullptr); - config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); - config_save_safe(App()->GetAppConfig(), "tmp", nullptr); - - config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); - config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); - config_save_safe(App()->GetUserConfig(), "tmp", nullptr); - #ifdef BROWSER_AVAILABLE DestroyPanelCookieManager(); delete cef; @@ -1658,127 +1646,61 @@ bool OBSBasic::ResetAudio() return obs_reset_audio2(&ai); } -void OBSBasic::closeEvent(QCloseEvent *event) +void OBSBasic::close() { - /* Wait for multitrack video stream to start/finish processing in the background */ - if (setupStreamingGuard.valid() && - setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { - QTimer::singleShot(1000, this, &OBSBasic::close); - event->ignore(); + if (isClosePromptOpen() || isClosing()) { return; } - /* Do not close window if inside of a temporary event loop because we - * could be inside of an Auth::LoadUI call. Keep trying once per - * second until we've exit any known sub-loops. */ - if (os_atomic_load_long(&insideEventLoop) != 0) { - QTimer::singleShot(1000, this, &OBSBasic::close); - event->ignore(); - return; - } + OBSMainWindow::close(); +} -#ifdef YOUTUBE_ENABLED - /* Also don't close the window if the youtube stream check is active */ - if (youtubeStreamCheckThread) { - QTimer::singleShot(1000, this, &OBSBasic::close); - event->ignore(); +void OBSBasic::closeEvent(QCloseEvent *event) +{ + if (isClosePromptOpen() || isClosing()) { return; } -#endif - if (isVisible()) + if (isVisible()) { config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", saveGeometry().toBase64().constData()); + } - bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); - - if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { - SetShowing(true); - - QMessageBox::StandardButton button = - OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + if (!isReadyToClose()) { + event->ignore(); - if (button == QMessageBox::No) { - event->ignore(); - restart = false; - return; - } + QTimer::singleShot(1000, this, &OBSBasic::close); + return; } if (remux && !remux->close()) { event->ignore(); restart = false; - return; - } - QWidget::closeEvent(event); - if (!event->isAccepted()) return; - - blog(LOG_INFO, SHUTDOWN_SEPARATOR); - - closing = true; - - /* While closing, a resize event to OBSQTDisplay could be triggered. - * The graphics thread on macOS dispatches a lambda function to be - * executed asynchronously in the main thread. However, the display is - * sometimes deleted before the lambda function is actually executed. - * To avoid such a case, destroy displays earlier than others such as - * deleting browser docks. */ - ui->preview->DestroyDisplay(); - if (program) - program->DestroyDisplay(); - - if (outputHandler->VirtualCamActive()) - outputHandler->StopVirtualCam(); - - if (introCheckThread) - introCheckThread->wait(); - if (whatsNewInitThread) - whatsNewInitThread->wait(); - if (updateCheckThread) - updateCheckThread->wait(); - if (logUploadThread) - logUploadThread->wait(); - if (devicePropertiesThread && devicePropertiesThread->isRunning()) { - devicePropertiesThread->wait(); - devicePropertiesThread.reset(); } - QApplication::sendPostedEvents(nullptr); - - signalHandlers.clear(); - - Auth::Save(); - SaveProjectNow(); - auth.reset(); - - delete extraBrowsers; - - config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); - -#ifdef BROWSER_AVAILABLE - if (cef) - SaveExtraBrowserDocks(); - - ClearExtraBrowserDocks(); -#endif - - OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + if (shouldPromptForClose()) { + event->ignore(); + restart = false; - disableSaving++; + if (!isClosePromptOpen()) { + bool shouldClose = promptToClose(); - /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, - * sources, etc) so that all references are released before shutdown */ - ClearSceneData(); + if (shouldClose) { + closeWindow(); + } + } - OnEvent(OBS_FRONTEND_EVENT_EXIT); + return; + } - // Destroys the frontend API so plugins can't continue calling it - obs_frontend_set_callbacks_internal(nullptr); - api = nullptr; + QWidget::closeEvent(event); + if (!event->isAccepted()) { + return; + } - QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); + closeWindow(); } bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) @@ -1899,6 +1821,162 @@ config_t *OBSBasic::Config() const return activeConfiguration; } +void OBSBasic::saveAll() +{ + if (isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + } + + Auth::Save(); + SaveProjectNow(); + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) { + SaveExtraBrowserDocks(); + } +#endif + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); +} + +bool OBSBasic::isReadyToClose() +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + return false; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + return false; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + return false; + } +#endif + + return true; +} + +bool OBSBasic::shouldPromptForClose() +{ + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + return true; + } + + return false; +} + +bool OBSBasic::promptToClose() +{ + isClosePromptOpen_ = true; + + SetShowing(true); + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text"), + QMessageBox::StandardButtons(QMessageBox::Ok | QMessageBox::Cancel)); + + if (button == QMessageBox::Cancel) { + isClosePromptOpen_ = false; + return false; + } + + isClosePromptOpen_ = false; + return true; +} + +void OBSBasic::closeWindow() +{ + if (isClosing()) { + return; + } + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + isClosing_ = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + delete extraBrowsers; + + saveAll(); + + auth.reset(); + +#ifdef BROWSER_AVAILABLE + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + applicationShutdown(); + deleteLater(); + + emit mainWindowClosed(); + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + void OBSBasic::UpdateEditMenu() { QModelIndexList items = GetAllSelectedSourceItems(); diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp index ca213433e0beb8..283e24c0005e96 100644 --- a/frontend/widgets/OBSBasic.hpp +++ b/frontend/widgets/OBSBasic.hpp @@ -259,7 +259,8 @@ class OBSBasic : public OBSMainWindow { std::vector signalHandlers; bool loaded = false; - bool closing = false; + bool isClosing_ = false; + bool isClosePromptOpen_ = false; bool handledShutdown = false; // TODO: Remove, orphaned variable @@ -300,6 +301,7 @@ class OBSBasic : public OBSMainWindow { void LoadProject(); public slots: + void close(); void UpdatePatronJson(const QString &text, const QString &error); void UpdateEditMenu(); void applicationShutdown() noexcept; @@ -325,13 +327,23 @@ public slots: void SetDisplayAffinity(QWindow *window); - inline bool Closing() { return closing; } + void saveAll(); + bool shouldPromptForClose(); + inline bool isClosing() { return isClosing_; } + inline bool isClosePromptOpen() { return isClosePromptOpen_; } + void closeWindow(); protected: + bool isReadyToClose(); + bool promptToClose(); + virtual void closeEvent(QCloseEvent *event) override; virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; virtual void changeEvent(QEvent *event) override; +signals: + void mainWindowClosed(); + /* ------------------------------------- * MARK: - OAuth * ------------------------------------- @@ -1123,6 +1135,7 @@ private slots: std::vector canvases; static void CanvasRemoved(void *data, calldata_t *params); + void ClearCanvases(); public: const std::vector &GetCanvases() const noexcept { return canvases; } diff --git a/frontend/widgets/OBSBasic_Canvases.cpp b/frontend/widgets/OBSBasic_Canvases.cpp index 9b1f346efa305a..4d8d8be54edbe7 100644 --- a/frontend/widgets/OBSBasic_Canvases.cpp +++ b/frontend/widgets/OBSBasic_Canvases.cpp @@ -33,15 +33,30 @@ const OBS::Canvas &OBSBasic::AddCanvas(const std::string &name, obs_video_info * bool OBSBasic::RemoveCanvas(OBSCanvas canvas) { + bool removed = false; if (!canvas) - return false; + return removed; auto canvas_it = std::find(std::begin(canvases), std::end(canvases), canvas); if (canvas_it != std::end(canvases)) { + // Move canvas to a temporary object to delay removal of the canvas and calls to its signal handlers + // until after erase() completes. This is to avoid issues with recursion coming from the + // CanvasRemoved() signal handler. + OBS::Canvas tmp = std::move(*canvas_it); canvases.erase(canvas_it); - OnEvent(OBS_FRONTEND_EVENT_CANVAS_REMOVED); - return true; + removed = true; } - return false; + if (removed) + OnEvent(OBS_FRONTEND_EVENT_CANVAS_REMOVED); + + return removed; +} + +void OBSBasic::ClearCanvases() +{ + // Delete canvases one-by-one to ensure OBS_FRONTEND_EVENT_CANVAS_REMOVED is sent for each + while (!canvases.empty()) { + RemoveCanvas(OBSCanvas(canvases.back())); + } } diff --git a/frontend/widgets/OBSBasic_SceneCollections.cpp b/frontend/widgets/OBSBasic_SceneCollections.cpp index ee9e4e6086f735..a18e69939de338 100644 --- a/frontend/widgets/OBSBasic_SceneCollections.cpp +++ b/frontend/widgets/OBSBasic_SceneCollections.cpp @@ -1003,7 +1003,7 @@ void OBSBasic::CreateDefaultScene(bool firstStart) ClearSceneData(); InitDefaultTransitions(); CreateDefaultQuickTransitions(); - transitionDuration = 300; + SetTransitionDuration(300); SetTransition(fadeTransition); updateRemigrationMenuItem(SceneCoordinateMode::Relative, ui->actionRemigrateSceneCollection); @@ -1303,7 +1303,7 @@ void OBSBasic::LoadData(obs_data_t *data, SceneCollection &collection) if (!curTransition) curTransition = fadeTransition; - transitionDuration = newDuration; + SetTransitionDuration(newDuration); SetTransition(curTransition); retryScene: @@ -1539,7 +1539,7 @@ void OBSBasic::ClearSceneData() obs_canvas_enum_scenes(canvas, cb, nullptr); } - canvases.clear(); + ClearCanvases(); OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); diff --git a/frontend/widgets/OBSBasic_Scenes.cpp b/frontend/widgets/OBSBasic_Scenes.cpp index 4976c7b98fe367..1797112fff9eb8 100644 --- a/frontend/widgets/OBSBasic_Scenes.cpp +++ b/frontend/widgets/OBSBasic_Scenes.cpp @@ -492,16 +492,21 @@ void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidge { OBSSource source; + bool forceSceneChange = false; + if (current) { OBSScene scene = GetOBSRef(current); source = obs_scene_get_source(scene); + bool oldSceneIsRemoved = obs_source_removed(obs_scene_get_source(currentScene)); + forceSceneChange = oldSceneIsRemoved; + currentScene = scene; } else { currentScene = NULL; } - SetCurrentScene(source); + SetCurrentScene(source, forceSceneChange); if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) outputHandler->UpdateVirtualCamOutputSource(); diff --git a/frontend/widgets/OBSBasic_Updater.cpp b/frontend/widgets/OBSBasic_Updater.cpp index b1322112869cc3..7b31f22becec37 100644 --- a/frontend/widgets/OBSBasic_Updater.cpp +++ b/frontend/widgets/OBSBasic_Updater.cpp @@ -63,8 +63,9 @@ template struct SignalContainer { void OBSBasic::ReceivedIntroJson(const QString &text) { #ifdef WHATSNEW_ENABLED - if (closing) + if (isClosing()) { return; + } WhatsNewList items; try { @@ -153,8 +154,9 @@ void OBSBasic::ReceivedIntroJson(const QString &text) void OBSBasic::ShowWhatsNew(const QString &url) { #ifdef BROWSER_AVAILABLE - if (closing) + if (isClosing()) { return; + } if (obsWhatsNew) { obsWhatsNew->close(); diff --git a/libobs/audio-monitoring/null/null-audio-monitoring.c b/libobs/audio-monitoring/null/null-audio-monitoring.c index 3bb38758cc6337..ea0536f9fc8583 100644 --- a/libobs/audio-monitoring/null/null-audio-monitoring.c +++ b/libobs/audio-monitoring/null/null-audio-monitoring.c @@ -26,3 +26,10 @@ void audio_monitor_destroy(struct audio_monitor *monitor) { UNUSED_PARAMETER(monitor); } + +bool devices_match(const char *id1, const char *id2) +{ + UNUSED_PARAMETER(id1); + UNUSED_PARAMETER(id2); + return false; +} diff --git a/libobs/audio-monitoring/osx/coreaudio-enum-devices.c b/libobs/audio-monitoring/osx/coreaudio-enum-devices.c index 0fd10bcd5647fd..a961d258e1540d 100644 --- a/libobs/audio-monitoring/osx/coreaudio-enum-devices.c +++ b/libobs/audio-monitoring/osx/coreaudio-enum-devices.c @@ -106,8 +106,8 @@ static bool alloc_default_id(void *data, const char *name, const char *id) static void get_default_id(char **p_id) { - AudioObjectPropertyAddress addr = {kAudioHardwarePropertyDefaultSystemOutputDevice, - kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain}; + AudioObjectPropertyAddress addr = {kAudioHardwarePropertyDefaultOutputDevice, kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMain}; if (*p_id) return; diff --git a/libobs/obs-audio.c b/libobs/obs-audio.c index 367d212db78e73..b3e2fb4709c664 100644 --- a/libobs/obs-audio.c +++ b/libobs/obs-audio.c @@ -48,54 +48,6 @@ static inline bool is_individual_audio_source(obs_source_t *source) !(source->info.output_flags & OBS_SOURCE_COMPOSITE); } -extern bool devices_match(const char *id1, const char *id2); - -static inline void check_audio_output_source_is_monitoring_device(obs_source_t *s, void *p) -{ - struct obs_core_audio *audio = p; - if (!audio->monitoring_device_name) - return; - - const char *id = s->info.id; - if (strcmp(id, "wasapi_output_capture") != 0 && strcmp(id, "pulse_output_capture") != 0 && - strcmp(id, "coreaudio_output_capture") != 0) - return; - - obs_data_t *settings = obs_source_get_settings(s); - if (!settings) - return; - - const char *device_id = obs_data_get_string(settings, "device_id"); - const char *mon_id = audio->monitoring_device_id; - bool id_match = false; - -#ifdef __APPLE__ - extern void get_desktop_default_id(char **p_id); - if (device_id && strcmp(device_id, "default") == 0) { - char *def_id = NULL; - get_desktop_default_id(&def_id); - id_match = devices_match(def_id, mon_id); - if (def_id) - bfree(def_id); - } else { - id_match = devices_match(device_id, mon_id); - } -#else - id_match = devices_match(device_id, mon_id); -#endif - - if (id_match) { - audio->prevent_monitoring_duplication = true; - audio->monitoring_duplicating_source = s; - if (!audio->monitoring_duplication_prevented_on_prev_tick) - blog(LOG_INFO, "Device for 'Audio Output Capture' source is also used for audio" - " monitoring:\nDeduplication logic is being applied to all monitored" - " sources.\n"); - } - - obs_data_release(settings); -} - /* * This version of push_audio_tree checks whether any source is an Audio Output Capture source ('Desktop Audio', * 'wasapi_output_capture' on Windows, 'pulse_output_capture' on Linux, 'coreaudio_output_capture' on macOS), & if the @@ -118,7 +70,6 @@ static void push_audio_tree2(obs_source_t *parent, obs_source_t *source, void *p if (s) { da_push_back(audio->render_order, &s); s->audio_is_duplicated = false; - check_audio_output_source_is_monitoring_device(s, audio); } } else { /* Source already present in tree → mark as duplicated if applicable */ @@ -558,23 +509,18 @@ static inline void execute_audio_tasks(void) /* In case monitoring and an 'Audio Output Capture' source have the same device, one silences all the monitored * sources unless the 'Audio Output Capture' is muted. - * The syncing between the mute state of the 'Audio Output Capture' source set in UI and libobs is a bit tricky. There - * is an intrinsic positive delay of the 'Audio Output Capture' source with respect to monitored sources due to the - * audio OS processing (wasapi, pulseaudio or coreaudio). Unfortunately, the delay is machine dependent and we can only - * mitigate its effects. During tests, src->user_muted worked better than src->muted, maybe because it is set earlier - * than the muted flag which allows to keep a better sync. With src->muted, unmuting the 'Audio Output Capture' led to - * a systematic level increase during one tick during testing for a sine tone (about +3 dBFS). In general we - * don't expect much difference though between user_muted and muted. */ static inline bool should_silence_monitored_source(obs_source_t *source, struct obs_core_audio *audio) { - if (!audio->monitoring_duplicating_source) + obs_source_t *dup_src = audio->monitoring_duplicating_source; + + if (!dup_src || !obs_source_active(dup_src)) return false; bool fader_muted = close_float(audio->monitoring_duplicating_source->volume, 0.0f, 0.0001f); - bool output_capture_unmuted = !audio->monitoring_duplicating_source->user_muted && !fader_muted; + bool output_capture_unmuted = !audio->monitoring_duplicating_source->muted && !fader_muted; - if (audio->prevent_monitoring_duplication && output_capture_unmuted) { + if (output_capture_unmuted) { if (source->monitoring_type == OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT && source != audio->monitoring_duplicating_source) { return true; @@ -617,9 +563,6 @@ bool audio_callback(void *param, uint64_t start_ts_in, uint64_t end_ts_in, uint6 da_resize(audio->render_order, 0); da_resize(audio->root_nodes, 0); - audio->monitoring_duplication_prevented_on_prev_tick = audio->prevent_monitoring_duplication; - audio->prevent_monitoring_duplication = false; - audio->monitoring_duplicating_source = NULL; deque_push_back(&audio->buffered_timestamps, &ts, sizeof(ts)); deque_peek_front(&audio->buffered_timestamps, &ts, sizeof(ts)); @@ -656,14 +599,11 @@ bool audio_callback(void *param, uint64_t start_ts_in, uint64_t end_ts_in, uint6 if (obs->video.mixes.array[j]->mix_audio) da_push_back(audio->root_nodes, &source); - /* Build audio tree, track Audio Output Capture sources and tag duplicate individual sources */ + /* Build audio tree, tag duplicate individual sources */ obs_source_enum_active_tree(source, push_audio_tree2, audio); /* add top - level sources to audio tree */ push_audio_tree(NULL, source, audio); - - /* Check whether the source is an 'Audio Output Capture' and coincides with monitoring device */ - check_audio_output_source_is_monitoring_device(source, audio); } pthread_mutex_unlock(&view->channels_mutex); } diff --git a/libobs/obs-config.h b/libobs/obs-config.h index 5fa1df03234d45..11df214f5905c5 100644 --- a/libobs/obs-config.h +++ b/libobs/obs-config.h @@ -41,7 +41,7 @@ * * Reset to zero each major or minor version */ -#define LIBOBS_API_PATCH_VER 2 +#define LIBOBS_API_PATCH_VER 4 #define MAKE_SEMANTIC_VERSION(major, minor, patch) ((major << 24) | (minor << 16) | patch) diff --git a/libobs/obs-internal.h b/libobs/obs-internal.h index 219293413f44df..aad8e22b12c8f4 100644 --- a/libobs/obs-internal.h +++ b/libobs/obs-internal.h @@ -460,9 +460,7 @@ struct obs_core_audio { pthread_mutex_t task_mutex; struct deque tasks; - volatile bool prevent_monitoring_duplication; struct obs_source *monitoring_duplicating_source; - bool monitoring_duplication_prevented_on_prev_tick; }; /* user sources, output channels, and displays */ diff --git a/libobs/obs-source-transition.c b/libobs/obs-source-transition.c index 92b5d107b9d11f..22f8f197346a8f 100644 --- a/libobs/obs-source-transition.c +++ b/libobs/obs-source-transition.c @@ -928,6 +928,12 @@ bool obs_transition_audio_render(obs_source_t *transition, uint64_t *ts_out, str sources[0] = transition->transition_sources[0]; sources[1] = transition->transition_sources[1]; + if (sources[0] && obs_source_removed(sources[0])) + sources[0] = NULL; + + if (sources[1] && obs_source_removed(sources[1])) + sources[1] = NULL; + min_ts = calc_min_ts(sources); if (min_ts) { diff --git a/libobs/obs-source.c b/libobs/obs-source.c index 533b91a5b33a5d..e5ef1304fdb7f4 100644 --- a/libobs/obs-source.c +++ b/libobs/obs-source.c @@ -368,6 +368,71 @@ static void obs_source_init_audio_hotkeys(struct obs_source *source) obs_source_hotkey_push_to_talk, source); } +void obs_source_audio_output_capture_device_activated(void *vptr, calldata_t *cd) +{ + UNUSED_PARAMETER(vptr); + obs_source_t *src = calldata_ptr(cd, "source"); + if (!src) + return; + + obs_data_t *settings = obs_source_get_settings(src); + const char *device_id = obs_data_get_string(settings, "device_id"); + obs_source_audio_output_capture_device_changed(src, device_id); + obs_data_release(settings); +} + +extern bool devices_match(const char *id1, const char *id2); +void obs_source_audio_output_capture_device_changed(obs_source_t *src, const char *device_id) +{ + struct obs_core_audio *audio = &obs->audio; + + if (!audio->monitoring_device_name) + return; + + if (!(src->info.output_flags & OBS_SOURCE_DO_NOT_SELF_MONITOR)) + return; + + const char *mon_id = audio->monitoring_device_id; + bool id_match = false; + +#ifdef __APPLE__ + extern void get_desktop_default_id(char **p_id); + if (device_id && strcmp(device_id, "default") == 0) { + char *def_id = NULL; + get_desktop_default_id(&def_id); + id_match = devices_match(def_id, mon_id); + if (def_id) + bfree(def_id); + } else { + id_match = devices_match(device_id, mon_id); + } +#else + id_match = devices_match(device_id, mon_id); +#endif + struct calldata cd; + uint8_t stack[128]; + calldata_init_fixed(&cd, stack, sizeof(stack)); + + if (id_match) { + calldata_set_ptr(&cd, "source", src); + signal_handler_signal(obs->signals, "deduplication_changed", &cd); + signal_handler_connect(src->context.signals, "activate", + obs_source_audio_output_capture_device_activated, NULL); + blog(LOG_INFO, + "Device for 'Audio Output Capture' source %s is also used for audio monitoring." + "\nDeduplication logic is being applied to all monitored sources.", + src->context.name); + } else { + if (src == audio->monitoring_duplicating_source) { + calldata_set_ptr(&cd, "source", NULL); + signal_handler_disconnect(src->context.signals, "activate", + obs_source_audio_output_capture_device_activated, NULL); + signal_handler_signal(obs->signals, "deduplication_changed", &cd); + blog(LOG_INFO, "Deduplication logic stopped."); + } + } +} + static obs_source_t *obs_source_create_internal(const char *id, const char *name, const char *uuid, obs_data_t *settings, obs_data_t *hotkey_data, bool private, uint32_t last_obs_ver, obs_canvas_t *canvas) @@ -5420,6 +5485,9 @@ bool obs_source_audio_pending(const obs_source_t *source) if (!obs_source_valid(source, "obs_source_audio_pending")) return true; + if (obs_source_removed(source)) + return true; + return (is_composite_source(source) || is_audio_source(source)) ? source->audio_pending : true; } diff --git a/libobs/obs.c b/libobs/obs.c index 6ea83a273cf326..e4688e67da8398 100644 --- a/libobs/obs.c +++ b/libobs/obs.c @@ -880,6 +880,22 @@ static void obs_free_graphics(void) } } +void set_monitoring_duplication_source(void *param) +{ + obs_source_t *src = param; + struct obs_core_audio *audio = &obs->audio; + + audio->monitoring_duplicating_source = src; +} + +static void apply_monitoring_deduplication(void *ignored, calldata_t *cd) +{ + UNUSED_PARAMETER(ignored); + obs_source_t *src = calldata_ptr(cd, "source"); + + obs_queue_task(OBS_TASK_AUDIO, set_monitoring_duplication_source, src, false); +} + static void set_audio_thread(void *unused); static bool obs_init_audio(struct audio_output_info *ai) @@ -899,7 +915,10 @@ static bool obs_init_audio(struct audio_output_info *ai) audio->monitoring_device_name = bstrdup("Default"); audio->monitoring_device_id = bstrdup("default"); - audio->monitoring_duplication_prevented_on_prev_tick = false; + audio->monitoring_duplicating_source = NULL; + + signal_handler_add(obs->signals, "void deduplication_changed(ptr source)"); + signal_handler_connect(obs->signals, "deduplication_changed", apply_monitoring_deduplication, NULL); errorcode = audio_output_open(&audio->audio, ai); if (errorcode == AUDIO_OUTPUT_SUCCESS) @@ -2947,6 +2966,18 @@ void obs_reset_audio_monitoring(void) pthread_mutex_unlock(&obs->audio.monitoring_mutex); } +static bool check_all_aoc_sources(void *param, obs_source_t *src) +{ + UNUSED_PARAMETER(param); + if (src->info.output_flags & OBS_SOURCE_DO_NOT_SELF_MONITOR) { + obs_data_t *settings = obs_source_get_settings(src); + const char *device_id = obs_data_get_string(settings, "device_id"); + obs_source_audio_output_capture_device_changed(src, device_id); + obs_data_release(settings); + } + return true; +} + bool obs_set_audio_monitoring_device(const char *name, const char *id) { if (!name || !id || !*name || !*id) @@ -2967,10 +2998,13 @@ bool obs_set_audio_monitoring_device(const char *name, const char *id) obs->audio.monitoring_device_name = bstrdup(name); obs->audio.monitoring_device_id = bstrdup(id); + pthread_mutex_unlock(&obs->audio.monitoring_mutex); obs_reset_audio_monitoring(); - pthread_mutex_unlock(&obs->audio.monitoring_mutex); + /* Check all Audio Output Capture sources for monitoring duplication. */ + obs_enum_sources(check_all_aoc_sources, NULL); + return true; } diff --git a/libobs/obs.h b/libobs/obs.h index 624b0362662e3c..9cb09b32e59ec9 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -1323,6 +1323,13 @@ EXPORT void obs_source_add_audio_capture_callback(obs_source_t *source, obs_sour EXPORT void obs_source_remove_audio_capture_callback(obs_source_t *source, obs_source_audio_capture_t callback, void *param); +/** + * For an Audio Output Capture source (like 'wasapi_output_capture') used for 'Desktop Audio', this checks whether the + * device is also used for monitoring. A signal to obs core struct is then emitted to trigger deduplication logic at + * the end of an audio tick. + */ +EXPORT void obs_source_audio_output_capture_device_changed(obs_source_t *source, const char *device_id); + typedef void (*obs_source_caption_t)(void *param, obs_source_t *source, const struct obs_source_cea_708 *captions); EXPORT void obs_source_add_caption_callback(obs_source_t *source, obs_source_caption_t callback, void *param); diff --git a/plugins/linux-pulseaudio/pulse-input.c b/plugins/linux-pulseaudio/pulse-input.c index 61c6227c90cb5d..5ff4c48b4df663 100644 --- a/plugins/linux-pulseaudio/pulse-input.c +++ b/plugins/linux-pulseaudio/pulse-input.c @@ -509,6 +509,11 @@ static void pulse_destroy(void *vptr) if (data->stream) pulse_stop_recording(data); + + /* If the device is also used for monitoring, a cleanup is needed. */ + if (!data->input) + obs_source_audio_output_capture_device_changed(data->source, NULL); + pulse_unref(); if (data->device) @@ -523,12 +528,15 @@ static void pulse_update(void *vptr, obs_data_t *settings) { PULSE_DATA(vptr); bool restart = false; - const char *new_device; - - new_device = obs_data_get_string(settings, "device_id"); + const char *new_device = obs_data_get_string(settings, "device_id"); if (!data->device || strcmp(data->device, new_device) != 0) { + /* Signal to deduplication logic in case the device is also used for monitoring. */ + if (!data->input) + obs_source_audio_output_capture_device_changed(data->source, new_device); + if (data->device) bfree(data->device); + data->device = bstrdup(new_device); data->is_default = strcmp("default", data->device) == 0; restart = true; diff --git a/plugins/mac-capture/mac-audio.c b/plugins/mac-capture/mac-audio.c index 6840166511e5e1..8d9dca9972fbe7 100644 --- a/plugins/mac-capture/mac-audio.c +++ b/plugins/mac-capture/mac-audio.c @@ -784,6 +784,9 @@ static void coreaudio_destroy(void *data) if (ca) { coreaudio_shutdown(ca); + /* If the device is also used for monitoring, a cleanup is needed. */ + if (!ca->input) + obs_source_audio_output_capture_device_changed(ca->source, NULL); os_event_destroy(ca->exit_event); @@ -818,11 +821,15 @@ static void coreaudio_set_channels(struct coreaudio_data *ca, obs_data_t *settin static void coreaudio_update(void *data, obs_data_t *settings) { struct coreaudio_data *ca = data; + const char *new_id = obs_data_get_string(settings, "device_id"); + + if (!ca->input && strcmp(new_id, ca->device_uid) != 0) + obs_source_audio_output_capture_device_changed(ca->source, new_id); coreaudio_shutdown(ca); bfree(ca->device_uid); - ca->device_uid = bstrdup(obs_data_get_string(settings, "device_id")); + ca->device_uid = bstrdup(new_id); ca->enable_downmix = obs_data_get_bool(settings, "enable_downmix"); @@ -865,6 +872,9 @@ static void *coreaudio_create(obs_data_t *settings, obs_source_t *source, bool i ca->device_uid = bstrdup("default"); coreaudio_try_init(ca); + if (!ca->input) + obs_source_audio_output_capture_device_changed(source, ca->device_uid); + return ca; } diff --git a/plugins/obs-webrtc/data/locale/en-US.ini b/plugins/obs-webrtc/data/locale/en-US.ini index c94717ec884366..246294691b4f92 100644 --- a/plugins/obs-webrtc/data/locale/en-US.ini +++ b/plugins/obs-webrtc/data/locale/en-US.ini @@ -4,3 +4,4 @@ Service.BearerToken="Bearer Token" Error.InvalidSDP="WHIP server responded with invalid SDP: %1" Error.NoRemoteDescription="Failed to set remote description: %1" +Error.SimulcastLayersRejected="WHIP server only accepted %1 simulcast layers" diff --git a/plugins/obs-webrtc/whip-output.cpp b/plugins/obs-webrtc/whip-output.cpp index b6d2ec2ce751a8..014919e7d61cb8 100644 --- a/plugins/obs-webrtc/whip-output.cpp +++ b/plugins/obs-webrtc/whip-output.cpp @@ -26,6 +26,9 @@ const uint8_t video_payload_type = 96; // ~3 seconds of 8.5 Megabit video const int video_nack_buffer_size = 4000; +const std::string rtpHeaderExtUriMid = "urn:ietf:params:rtp-hdrext:sdes:mid"; +const std::string rtpHeaderExtUriRid = "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"; + WHIPOutput::WHIPOutput(obs_data_t *, obs_output_t *output) : output(output), endpoint_url(), @@ -41,8 +44,7 @@ WHIPOutput::WHIPOutput(obs_data_t *, obs_output_t *output) total_bytes_sent(0), connect_time_ms(0), start_time_ns(0), - last_audio_timestamp(0), - last_video_timestamp(0) + last_audio_timestamp(0) { } @@ -59,6 +61,19 @@ bool WHIPOutput::Start() { std::lock_guard l(start_stop_mutex); + for (uint32_t idx = 0; idx < MAX_OUTPUT_VIDEO_ENCODERS; idx++) { + auto encoder = obs_output_get_video_encoder2(output, idx); + if (encoder == nullptr) { + break; + } + + auto v = std::make_shared(); + // base_ssrc is ssrc for audio track. We do `+ 1` for the video, then idx for each Simulcast layer. + v->ssrc = base_ssrc + 1 + idx; + v->rid = std::to_string(idx); + videoLayerStates[encoder] = v; + } + if (!obs_output_can_begin_data_capture(output, 0)) return false; if (!obs_output_initialize_encoders(output, 0)) @@ -93,9 +108,25 @@ void WHIPOutput::Data(struct encoder_packet *packet) Send(packet->data, packet->size, duration, audio_track, audio_sr_reporter); last_audio_timestamp = packet->dts_usec; } else if (video_track && packet->type == OBS_ENCODER_VIDEO) { - int64_t duration = packet->dts_usec - last_video_timestamp; + auto rtp_config = video_sr_reporter->rtpConfig; + auto videoLayerState = videoLayerStates[packet->encoder]; + if (videoLayerState == nullptr) { + Stop(false); + obs_output_signal_stop(output, OBS_OUTPUT_ENCODE_ERROR); + return; + } + + rtp_config->sequenceNumber = videoLayerState->sequenceNumber; + rtp_config->ssrc = videoLayerState->ssrc; + rtp_config->rid = videoLayerState->rid; + rtp_config->timestamp = videoLayerState->rtpTimestamp; + int64_t duration = packet->dts_usec - videoLayerState->lastVideoTimestamp; + Send(packet->data, packet->size, duration, video_track, video_sr_reporter); - last_video_timestamp = packet->dts_usec; + + videoLayerState->sequenceNumber = rtp_config->sequenceNumber; + videoLayerState->lastVideoTimestamp = packet->dts_usec; + videoLayerState->rtpTimestamp = rtp_config->timestamp; } } @@ -142,6 +173,24 @@ void WHIPOutput::ConfigureVideoTrack(std::string media_stream_id, std::string cn rtc::Description::Video video_description(video_mid, rtc::Description::Direction::SendOnly); video_description.addSSRC(ssrc, cname, media_stream_id, media_stream_track_id); + video_description.addExtMap(rtc::Description::Entry::ExtMap(1, rtpHeaderExtUriMid)); + video_description.addExtMap(rtc::Description::Entry::ExtMap(2, rtpHeaderExtUriRid)); + + if (videoLayerStates.size() >= 2) { + std::vector> sortedRids; + + for (const auto &[encoder, state] : videoLayerStates) { + sortedRids.push_back({std::stoi(state->rid), state->rid}); + } + + std::sort(sortedRids.begin(), sortedRids.end(), + [](const auto &a, const auto &b) { return a.first < b.first; }); + + for (const auto &[_, rid] : sortedRids) { + video_description.addRid(rid); + } + } + auto rtp_config = std::make_shared(ssrc, cname, video_payload_type, #if RTC_VERSION_MAJOR == 0 && RTC_VERSION_MINOR > 22 || RTC_VERSION_MAJOR > 0 rtc::H264RtpPacketizer::ClockRate); @@ -149,6 +198,10 @@ void WHIPOutput::ConfigureVideoTrack(std::string media_stream_id, std::string cn rtc::H264RtpPacketizer::defaultClockRate); #endif + rtp_config->midId = 1; + rtp_config->ridId = 2; + rtp_config->mid = video_mid; + const obs_encoder_t *encoder = obs_output_get_video_encoder2(output, 0); if (!encoder) return; @@ -372,16 +425,26 @@ bool WHIPOutput::Connect() curl_easy_setopt(c, CURLOPT_UNRESTRICTED_AUTH, 1L); curl_easy_setopt(c, CURLOPT_ERRORBUFFER, error_buffer); - auto cleanup = [&]() { + auto doCleanup = [&](bool connectFailed) { curl_easy_cleanup(c); curl_slist_free_all(headers); + if (connectFailed) { + obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); + } + }; + + auto displayError = [&](const char *what, const char *errorMessage) { + struct dstr error_message; + dstr_init_copy(&error_message, obs_module_text(errorMessage)); + dstr_replace(&error_message, "%1", what); + obs_output_set_last_error(output, error_message.array); + dstr_free(&error_message); }; CURLcode res = curl_easy_perform(c); if (res != CURLE_OK) { do_log(LOG_ERROR, "Connect failed: %s", error_buffer[0] ? error_buffer : curl_easy_strerror(res)); - cleanup(); - obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); + doCleanup(true); return false; } @@ -389,15 +452,14 @@ bool WHIPOutput::Connect() curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &response_code); if (response_code != 201) { do_log(LOG_ERROR, "Connect failed: HTTP endpoint returned response code %ld", response_code); - cleanup(); + doCleanup(false); obs_output_signal_stop(output, OBS_OUTPUT_INVALID_STREAM); return false; } if (read_buffer.empty()) { do_log(LOG_ERROR, "Connect failed: No data returned from HTTP endpoint request"); - cleanup(); - obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); + doCleanup(true); return false; } @@ -417,8 +479,7 @@ bool WHIPOutput::Connect() if (location_header_count < static_cast(redirect_count) + 1) { do_log(LOG_ERROR, "WHIP server did not provide a resource URL via the Location header"); - cleanup(); - obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); + doCleanup(true); return false; } @@ -446,8 +507,7 @@ bool WHIPOutput::Connect() curl_easy_getinfo(c, CURLINFO_EFFECTIVE_URL, &effective_url); if (effective_url == nullptr) { do_log(LOG_ERROR, "Failed to build Resource URL"); - cleanup(); - obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); + doCleanup(true); return false; } @@ -462,8 +522,7 @@ bool WHIPOutput::Connect() CURLUcode rc = curl_url_get(url_builder, CURLUPART_URL, &url, CURLU_NO_DEFAULT_PORT); if (rc) { do_log(LOG_ERROR, "WHIP server provided a invalid resource URL via the Location header"); - cleanup(); - obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); + doCleanup(true); return false; } @@ -479,31 +538,40 @@ bool WHIPOutput::Connect() auto response = std::string(read_buffer); response.erase(0, response.find("v=0")); + // If we are sending multiple layers assert that the remote accepted them all + if (videoLayerStates.size() != 1) { + auto layersAccepted = simulcast_layers_in_answer(response); + if (videoLayerStates.size() != layersAccepted) { + do_log(LOG_ERROR, "WHIP only accepted %lu layers", layersAccepted); + displayError(std::to_string(layersAccepted).c_str(), "Error.SimulcastLayersRejected"); + doCleanup(true); + return false; + } + } + rtc::Description answer(response, "answer"); try { peer_connection->setRemoteDescription(answer); } catch (const std::invalid_argument &err) { do_log(LOG_ERROR, "WHIP server responded with invalid SDP: %s", err.what()); - cleanup(); + doCleanup(true); struct dstr error_message; dstr_init_copy(&error_message, obs_module_text("Error.InvalidSDP")); dstr_replace(&error_message, "%1", err.what()); obs_output_set_last_error(output, error_message.array); dstr_free(&error_message); - obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); return false; } catch (const std::exception &err) { do_log(LOG_ERROR, "Failed to set remote description: %s", err.what()); - cleanup(); + doCleanup(true); struct dstr error_message; dstr_init_copy(&error_message, obs_module_text("Error.NoRemoteDescription")); dstr_replace(&error_message, "%1", err.what()); obs_output_set_last_error(output, error_message.array); dstr_free(&error_message); - obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); return false; } - cleanup(); + doCleanup(false); #if RTC_VERSION_MAJOR == 0 && RTC_VERSION_MINOR > 20 || RTC_VERSION_MAJOR > 0 peer_connection->gatherLocalCandidates(iceServers); @@ -557,7 +625,7 @@ void WHIPOutput::SendDelete() curl_easy_setopt(c, CURLOPT_TIMEOUT, 8L); curl_easy_setopt(c, CURLOPT_ERRORBUFFER, error_buffer); - auto cleanup = [&]() { + auto doCleanup = [&]() { curl_easy_cleanup(c); curl_slist_free_all(headers); }; @@ -566,7 +634,7 @@ void WHIPOutput::SendDelete() if (res != CURLE_OK) { do_log(LOG_WARNING, "DELETE request for resource URL failed: %s", error_buffer[0] ? error_buffer : curl_easy_strerror(res)); - cleanup(); + doCleanup(); return; } @@ -574,13 +642,13 @@ void WHIPOutput::SendDelete() curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &response_code); if (response_code != 200) { do_log(LOG_WARNING, "DELETE request for resource URL failed. HTTP Code: %ld", response_code); - cleanup(); + doCleanup(); return; } do_log(LOG_DEBUG, "Successfully performed DELETE request for resource URL"); resource_url.clear(); - cleanup(); + doCleanup(); } void WHIPOutput::StopThread(bool signal) @@ -611,7 +679,7 @@ void WHIPOutput::StopThread(bool signal) connect_time_ms = 0; start_time_ns = 0; last_audio_timestamp = 0; - last_video_timestamp = 0; + videoLayerStates.clear(); } void WHIPOutput::Send(void *data, uintptr_t size, uint64_t duration, std::shared_ptr track, @@ -652,7 +720,7 @@ void WHIPOutput::Send(void *data, uintptr_t size, uint64_t duration, std::shared void register_whip_output() { - const uint32_t base_flags = OBS_OUTPUT_ENCODED | OBS_OUTPUT_SERVICE; + const uint32_t base_flags = OBS_OUTPUT_ENCODED | OBS_OUTPUT_SERVICE | OBS_OUTPUT_MULTI_TRACK_AV; const char *audio_codecs = "opus"; #ifdef ENABLE_HEVC diff --git a/plugins/obs-webrtc/whip-output.h b/plugins/obs-webrtc/whip-output.h index 2db60d65578fc3..78f078dedfd7b6 100644 --- a/plugins/obs-webrtc/whip-output.h +++ b/plugins/obs-webrtc/whip-output.h @@ -10,9 +10,18 @@ #include #include #include +#include #include +struct videoLayerState { + uint16_t sequenceNumber; + uint32_t rtpTimestamp; + int64_t lastVideoTimestamp; + uint32_t ssrc; + std::string rid; +}; + class WHIPOutput { public: WHIPOutput(obs_data_t *settings, obs_output_t *output); @@ -36,7 +45,6 @@ class WHIPOutput { void SendDelete(); void StopThread(bool signal); void ParseLinkHeader(std::string linkHeader, std::vector &iceServers); - void Send(void *data, uintptr_t size, uint64_t duration, std::shared_ptr track, std::shared_ptr rtcp_sr_reporter); @@ -58,11 +66,12 @@ class WHIPOutput { std::shared_ptr audio_sr_reporter; std::shared_ptr video_sr_reporter; + std::map> videoLayerStates; + std::atomic total_bytes_sent; std::atomic connect_time_ms; int64_t start_time_ns; int64_t last_audio_timestamp; - int64_t last_video_timestamp; }; void register_whip_output(); diff --git a/plugins/obs-webrtc/whip-utils.h b/plugins/obs-webrtc/whip-utils.h index 353cab65b5a525..c76f1f906ddfc4 100644 --- a/plugins/obs-webrtc/whip-utils.h +++ b/plugins/obs-webrtc/whip-utils.h @@ -83,3 +83,25 @@ static inline std::string generate_user_agent() return ua.str(); } + +static size_t simulcast_layers_in_answer(std::string answer) +{ + auto layersStart = answer.find("a=simulcast"); + if (layersStart == std::string::npos) { + return 0; + } + + auto layersEnd = answer.find("\r\n", layersStart); + if (layersEnd == std::string::npos) { + return 0; + } + + size_t layersAccepted = 1; + for (auto i = layersStart; i < layersEnd; i++) { + if (answer[i] == ';') { + layersAccepted++; + } + } + + return layersAccepted; +} diff --git a/plugins/win-wasapi/win-wasapi.cpp b/plugins/win-wasapi/win-wasapi.cpp index e68d7c39b61259..0cea4758559b2f 100644 --- a/plugins/win-wasapi/win-wasapi.cpp +++ b/plugins/win-wasapi/win-wasapi.cpp @@ -464,6 +464,9 @@ WASAPISource::~WASAPISource() if (notify) { notify->RemoveDefaultDeviceChangedCallback(this); } + // If the device is also used for monitoring, a cleanup is needed. + if (sourceType == SourceType::DeviceOutput) + obs_source_audio_output_capture_device_changed(source, NULL); Stop(); } @@ -503,6 +506,10 @@ WASAPISource::UpdateParams WASAPISource::BuildUpdateParams(obs_data_t *settings) void WASAPISource::UpdateSettings(UpdateParams &¶ms) { + // Signal to deduplication logic in case the device is also used for monitoring. + if (device_id != params.device_id && sourceType == SourceType::DeviceOutput) + obs_source_audio_output_capture_device_changed(source, params.device_id.c_str()); + device_id = std::move(params.device_id); useDeviceTiming = params.useDeviceTiming; isDefaultDevice = params.isDefaultDevice;